mirror of
https://gitee.com/ThingsGateway/ThingsGateway.git
synced 2025-10-21 11:08:13 +08:00
Compare commits
40 Commits
10.10.9.0
...
10.11.21.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
5436b91c89 | ||
![]() |
d1c46f51a6 | ||
![]() |
4539d8d198 | ||
![]() |
9ea9529a5f | ||
![]() |
4e6be23aac | ||
![]() |
2fabbd236b | ||
![]() |
163a66530e | ||
![]() |
29073a00c4 | ||
![]() |
c6d4d1ecfa | ||
![]() |
ba16889cad | ||
![]() |
5aaed35b0f | ||
![]() |
df067c91eb | ||
![]() |
2078b4a60b | ||
![]() |
20a2e3ff8e | ||
![]() |
61a973b1b5 | ||
![]() |
cbd72e2081 | ||
![]() |
4e0377b20c | ||
![]() |
fd318d3cdc | ||
![]() |
515bdb9700 | ||
![]() |
46c1780017 | ||
![]() |
fe78a4c3ca | ||
![]() |
2d7effadf9 | ||
![]() |
346c560f8b | ||
![]() |
8e3bd89f61 | ||
![]() |
6da142d080 | ||
![]() |
ff7d029e6f | ||
![]() |
21b4695683 | ||
![]() |
02ad494a26 | ||
![]() |
280366e1b2 | ||
![]() |
6660ce3e34 | ||
![]() |
7499162c1a | ||
![]() |
40208a5cd6 | ||
![]() |
fa347f4f68 | ||
![]() |
d7df6fc605 | ||
![]() |
eb4bb2fd48 | ||
![]() |
faa9858974 | ||
![]() |
1b3d2dda49 | ||
![]() |
a8a9453611 | ||
![]() |
e84f42ce14 | ||
![]() |
6f814cf6b8 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -364,6 +364,7 @@ FodyWeavers.xsd
|
||||
|
||||
/src/*Pro*/
|
||||
/src/*Pro*
|
||||
/src/**/*Pro*
|
||||
/src/*pro*
|
||||
/src/*pro*/
|
||||
/src/ThingsGateway.Server/Configuration/GiteeOAuthSettings.json
|
||||
|
@@ -11,6 +11,7 @@
|
||||
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||
<!-- 避免 DLL 被打包到 lib/ -->
|
||||
<EnableSourceGenerator>true</EnableSourceGenerator>
|
||||
|
||||
<!-- 可选 -->
|
||||
|
||||
|
||||
|
@@ -37,9 +37,8 @@ public class FileController : ControllerBase
|
||||
var root = Directory.GetCurrentDirectory();
|
||||
var wwwroot = Path.Combine(root, "wwwroot");
|
||||
var filePath = Path.Combine(wwwroot, fileName);
|
||||
// 防止路径穿越攻击
|
||||
#pragma warning disable CA3003
|
||||
if (!filePath.StartsWith(wwwroot, StringComparison.OrdinalIgnoreCase) || !System.IO.File.Exists(filePath))
|
||||
if ((!(fileName.StartsWith(@"../Logs") || fileName.StartsWith(@"..\Logs")) && filePath.Contains("..")) || !System.IO.File.Exists(filePath))
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
@@ -49,6 +48,6 @@ public class FileController : ControllerBase
|
||||
|
||||
Response.Headers.Append("Access-Control-Expose-Headers", "Content-Disposition");
|
||||
|
||||
return File(fileStream, "application/octet-stream", (fileName.Replace('/', '_')));
|
||||
return File(fileStream, "application/octet-stream", (Path.GetFileName(filePath).Replace('/', '_')));
|
||||
}
|
||||
}
|
||||
|
@@ -21,16 +21,7 @@
|
||||
"UserNoModule": "This account has not been assigned a module. Please contact the administrator",
|
||||
"UserNull": "User {0} does not exist"
|
||||
},
|
||||
"ThingsGateway.Admin.Application.BaseDataEntity": {
|
||||
"CreateOrgId": "CreateOrgId"
|
||||
},
|
||||
"ThingsGateway.Admin.Application.BaseEntity": {
|
||||
"CreateTime": "CreateTime",
|
||||
"CreateUser": "CreateUser",
|
||||
"SortCode": "SortCode",
|
||||
"UpdateTime": "UpdateTime",
|
||||
"UpdateUser": "UpdateUser"
|
||||
},
|
||||
|
||||
"ThingsGateway.Admin.Application.BlazorAuthenticationHandler": {
|
||||
"UserExpire": "User expired, please login again"
|
||||
},
|
||||
|
@@ -21,16 +21,7 @@
|
||||
"UserNoModule": "该账号未分配模块,请联系管理员",
|
||||
"UserNull": "用户 {0} 不存在"
|
||||
},
|
||||
"ThingsGateway.Admin.Application.BaseDataEntity": {
|
||||
"CreateOrgId": "创建机构Id"
|
||||
},
|
||||
"ThingsGateway.Admin.Application.BaseEntity": {
|
||||
"CreateTime": "创建时间",
|
||||
"CreateUser": "创建人",
|
||||
"SortCode": "排序",
|
||||
"UpdateTime": "更新时间",
|
||||
"UpdateUser": "更新人"
|
||||
},
|
||||
|
||||
"ThingsGateway.Admin.Application.BlazorAuthenticationHandler": {
|
||||
"UserExpire": "用户登录已过期,请重新登录"
|
||||
},
|
||||
|
@@ -377,9 +377,9 @@ internal sealed class SysUserService : BaseService<SysUser>, ISysUserService
|
||||
/// 获取用户拥有的资源
|
||||
/// </summary>
|
||||
/// <param name="id">用户id</param>
|
||||
public async Task<GrantResourceData> OwnResourceAsync(long id)
|
||||
public Task<GrantResourceData> OwnResourceAsync(long id)
|
||||
{
|
||||
return await _roleService.OwnResourceAsync(id, RelationCategoryEnum.UserHasResource).ConfigureAwait(false);
|
||||
return _roleService.OwnResourceAsync(id, RelationCategoryEnum.UserHasResource);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -505,10 +505,10 @@ internal sealed class SysUserService : BaseService<SysUser>, ISysUserService
|
||||
var password = await GetDefaultPassWord(true).ConfigureAwait(false);//获取默认密码,这里不走Aop所以需要加密一下
|
||||
using var db = GetDB();
|
||||
//重置密码
|
||||
if (await db.UpdateSetColumnsTrueAsync<SysUser>(it => new SysUser
|
||||
if ((await db.UpdateSetColumnsTrueAsync<SysUser>(it => new SysUser
|
||||
{
|
||||
Password = password
|
||||
}, it => it.Id == id).ConfigureAwait(false))
|
||||
}, it => it.Id == id).ConfigureAwait(false)) > 0)
|
||||
{
|
||||
DeleteUserFromCache(id);//从cache删除用户信息
|
||||
var verificatInfoIds = _verificatInfoService.GetListByUserId(id);
|
||||
|
@@ -185,12 +185,12 @@ internal sealed class UserCenterService : BaseService<SysUser>, IUserCenterServi
|
||||
using var db = GetDB();
|
||||
|
||||
//更新指定字段
|
||||
var result = await db.UpdateSetColumnsTrueAsync<SysUser>(it => new SysUser
|
||||
var result = (await db.UpdateSetColumnsTrueAsync<SysUser>(it => new SysUser
|
||||
{
|
||||
Email = input.Email,
|
||||
Phone = input.Phone,
|
||||
Avatar = input.Avatar,
|
||||
}, it => it.Id == UserManager.UserId).ConfigureAwait(false);
|
||||
}, it => it.Id == UserManager.UserId).ConfigureAwait(false)) > 0;
|
||||
if (result)
|
||||
_userService.DeleteUserFromCache(UserManager.UserId);//cache删除用户数据
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
|
||||
|
@@ -30,7 +30,7 @@ public class BlazorAppContext
|
||||
/// <summary>
|
||||
/// 全部菜单
|
||||
/// </summary>
|
||||
public IEnumerable<SysResource> AllMenus { get; private set; }
|
||||
public List<SysResource> AllMenus { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前用户
|
||||
@@ -42,22 +42,22 @@ public class BlazorAppContext
|
||||
/// <summary>
|
||||
/// 用户个人菜单
|
||||
/// </summary>
|
||||
public IEnumerable<MenuItem> OwnMenuItems { get; private set; }
|
||||
public List<MenuItem> OwnMenuItems { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 不同模块的菜单
|
||||
/// </summary>
|
||||
public IEnumerable<MenuItem> AllOwnMenuItems { get; private set; }
|
||||
public List<MenuItem> AllOwnMenuItems { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户个人菜单,多个模块
|
||||
/// </summary>
|
||||
public IEnumerable<SysResource> OwnMenus { get; private set; }
|
||||
public List<SysResource> OwnMenus { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户个人菜单,非树形
|
||||
/// </summary>
|
||||
public IEnumerable<MenuItem> OwnSameLevelMenuItems { get; private set; }
|
||||
public List<MenuItem> OwnSameLevelMenuItems { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 个人工作台
|
||||
@@ -67,9 +67,9 @@ public class BlazorAppContext
|
||||
/// <summary>
|
||||
/// 用户个人快捷方式菜单
|
||||
/// </summary>
|
||||
public IEnumerable<SysResource> UserWorkbenchOutputs { get; private set; }
|
||||
public List<SysResource> UserWorkbenchOutputs { get; private set; }
|
||||
|
||||
public IEnumerable<SysResource> AllResource { get; private set; }
|
||||
public List<SysResource> AllResource { get; private set; }
|
||||
|
||||
private ISysResourceService ResourceService { get; }
|
||||
private ISysUserService SysUserService { get; }
|
||||
@@ -93,7 +93,7 @@ public class BlazorAppContext
|
||||
AllResource = sysResources;
|
||||
var ids = CurrentUser.ModuleList.Select(a => a.Id).ToHashSet();
|
||||
CurrentUser.ModuleList = AllResource.Where(a => ids.Contains(a.Id)).OrderBy(a => a.SortCode).ToList();
|
||||
AllMenus = AllResource.Where(a => a.Category == ResourceCategoryEnum.Menu);
|
||||
AllMenus = AllResource.Where(a => a.Category == ResourceCategoryEnum.Menu).ToList();
|
||||
|
||||
if (moduleId == null)
|
||||
{
|
||||
@@ -123,8 +123,8 @@ public class BlazorAppContext
|
||||
}
|
||||
}
|
||||
var ownMenus = OwnMenus.Where(a => a.Module == CurrentModuleId);
|
||||
OwnMenuItems = ResourceUtil.BuildMenuTrees(ownMenus).ToList();
|
||||
AllOwnMenuItems = ResourceUtil.BuildMenuTrees(OwnMenus).ToList();
|
||||
OwnMenuItems = AdminResourceUtil.BuildMenuTrees(ownMenus).ToList();
|
||||
AllOwnMenuItems = AdminResourceUtil.BuildMenuTrees(OwnMenus).ToList();
|
||||
OwnSameLevelMenuItems = ownMenus.Where(a => !a.Href.IsNullOrWhiteSpace()).Select(item => new MenuItem()
|
||||
{
|
||||
Match = item.NavLinkMatch ?? Microsoft.AspNetCore.Components.Routing.NavLinkMatch.All,
|
||||
@@ -132,8 +132,8 @@ public class BlazorAppContext
|
||||
Icon = item.Icon,
|
||||
Url = item.Href,
|
||||
Target = item.Target.ToString(),
|
||||
});
|
||||
UserWorkbenchOutputs = AllMenus.Where(it => UserWorkBench.Shortcuts.Contains(it.Id));
|
||||
}).ToList();
|
||||
UserWorkbenchOutputs = AllMenus.Where(it => UserWorkBench.Shortcuts.Contains(it.Id)).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -29,7 +29,7 @@ public partial class EditPagePolicy
|
||||
|
||||
protected override Task OnParametersSetAsync()
|
||||
{
|
||||
ShortcutsTreeViewItems = ResourceUtil.BuildTreeItemList(AppContext.OwnMenus.WhereIf(!ShortcutsSearchText.IsNullOrEmpty(), a => a.Title.Contains(ShortcutsSearchText)), Model.Shortcuts, null);
|
||||
ShortcutsTreeViewItems = AdminResourceUtil.BuildTreeItemList(AppContext.OwnMenus.WhereIf(!ShortcutsSearchText.IsNullOrEmpty(), a => a.Title.Contains(ShortcutsSearchText)), Model.Shortcuts, null);
|
||||
return base.OnParametersSetAsync();
|
||||
}
|
||||
|
||||
@@ -48,6 +48,6 @@ public partial class EditPagePolicy
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
ShortcutsSearchText = searchText;
|
||||
return ResourceUtil.BuildTreeItemList(AppContext.OwnMenus.WhereIf(!ShortcutsSearchText.IsNullOrEmpty(), a => a.Title.Contains(ShortcutsSearchText)), Model.Shortcuts, null);
|
||||
return AdminResourceUtil.BuildTreeItemList(AppContext.OwnMenus.WhereIf(!ShortcutsSearchText.IsNullOrEmpty(), a => a.Title.Contains(ShortcutsSearchText)), Model.Shortcuts, null);
|
||||
}
|
||||
}
|
||||
|
@@ -41,7 +41,7 @@ public partial class MenuChoiceDialog
|
||||
var all = (await SysResourceService.GetAllAsync());
|
||||
var items = all.Where(a => a.Category == ResourceCategoryEnum.Menu && a.Module == ModuleId);
|
||||
ModuleTitle = all.FirstOrDefault(a => a.Id == ModuleId)?.Title;
|
||||
Items = ResourceUtil.BuildTreeItemList(items, new List<long> { Value }, RenderTreeItem);
|
||||
Items = AdminResourceUtil.BuildTreeItemList(items, new List<long> { Value }, RenderTreeItem);
|
||||
await base.OnParametersSetAsync();
|
||||
}
|
||||
|
||||
|
@@ -26,12 +26,12 @@
|
||||
OnQueryAsync="OnQueryAsync" CustomerSearchModel="@CustomerSearchModel"
|
||||
OnSaveAsync="Save" OnDeleteAsync="Delete">
|
||||
<TableToolbarTemplate>
|
||||
<PopConfirmButton Color=Color.Warning IsDisabled="SelectedRows.Count<=0||!AuthorizeButton(AdminOperConst.Add)" Text=@OperDescLocalizer["CopyResource"] Icon="fa fa-copy" OnConfirm="OnCopy">
|
||||
<PopConfirmButton Color=Color.Warning IsKeepDisabled="SelectedRows.Count <= 0 || !AuthorizeButton(AdminOperConst.Add)" Text=@OperDescLocalizer["CopyResource"] Icon="fa fa-copy" OnConfirm="OnCopy">
|
||||
<BodyTemplate>
|
||||
<Select Items="ModuleSelectedItems" @bind-Value=CopyModule ShowLabel="false" />
|
||||
</BodyTemplate>
|
||||
</PopConfirmButton>
|
||||
<PopConfirmButton Color=Color.Warning IsDisabled="SelectedRows.Count!=1||!AuthorizeButton(AdminOperConst.Edit)" Text=@OperDescLocalizer["ChangeParentResource"] Icon="fa fa-copy" OnConfirm="OnChangeParent">
|
||||
<PopConfirmButton Color=Color.Warning IsKeepDisabled="SelectedRows.Count != 1 || !AuthorizeButton(AdminOperConst.Edit)" Text=@OperDescLocalizer["ChangeParentResource"] Icon="fa fa-copy" OnConfirm="OnChangeParent">
|
||||
<BodyTemplate>
|
||||
<div class="overflow-y-auto" style="height:500px">
|
||||
<TreeView Items="MenuTreeItems" IsVirtualize="true" OnTreeItemClick="a=>{ChangeParentId=a.Value.Id;return Task.CompletedTask;}" />
|
||||
|
@@ -39,8 +39,8 @@ public partial class SysResourcePage
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
ModuleSelectedItems = ResourceUtil.BuildModuleSelectList((await SysResourceService.GetAllAsync())).ToList();
|
||||
MenuItems = ResourceUtil.BuildMenuSelectList((await SysResourceService.GetAllAsync())).Concat(new List<SelectedItem>() { new("0", AdminLocalizer["Root"]) }).ToList();
|
||||
ModuleSelectedItems = AdminResourceUtil.BuildModuleSelectList((await SysResourceService.GetAllAsync())).ToList();
|
||||
MenuItems = AdminResourceUtil.BuildMenuSelectList((await SysResourceService.GetAllAsync())).Concat(new List<SelectedItem>() { new("0", AdminLocalizer["Root"]) }).ToList();
|
||||
|
||||
await base.OnParametersSetAsync();
|
||||
}
|
||||
@@ -49,7 +49,7 @@ public partial class SysResourcePage
|
||||
|
||||
private async Task<QueryData<SysResource>> OnQueryAsync(QueryPageOptions options)
|
||||
{
|
||||
MenuTreeItems = new List<TreeViewItem<SysResource>>() { new TreeViewItem<SysResource>(new SysResource()) { Text = AdminLocalizer["Root"] } }.Concat(ResourceUtil.BuildTreeItemList((await SysResourceService.GetAllAsync()).Where(a => a.Module == CustomerSearchModel.Module), new(), null)).ToList();
|
||||
MenuTreeItems = new List<TreeViewItem<SysResource>>() { new TreeViewItem<SysResource>(new SysResource()) { Text = AdminLocalizer["Root"] } }.Concat(AdminResourceUtil.BuildTreeItemList((await SysResourceService.GetAllAsync()).Where(a => a.Module == CustomerSearchModel.Module), new(), null)).ToList();
|
||||
|
||||
var data = await SysResourceService.PageAsync(options, CustomerSearchModel);
|
||||
return data;
|
||||
@@ -136,14 +136,14 @@ public partial class SysResourcePage
|
||||
private async Task<IEnumerable<TableTreeNode<SysResource>>> OnTreeExpand(SysResource menu)
|
||||
{
|
||||
var sysResources = await SysResourceService.GetAllAsync();
|
||||
var result = ResourceUtil.BuildTableTrees(sysResources, menu.Id);
|
||||
var result = AdminResourceUtil.BuildTableTrees(sysResources, menu.Id);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static async Task<IEnumerable<TableTreeNode<SysResource>>> TreeNodeConverter(IEnumerable<SysResource> items)
|
||||
{
|
||||
await Task.CompletedTask;
|
||||
var result = ResourceUtil.BuildTableTrees(items, 0);
|
||||
var result = AdminResourceUtil.BuildTableTrees(items, 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@@ -35,7 +35,7 @@ public partial class GrantResourceDialog
|
||||
{
|
||||
var items = (await SysResourceService.GetAllAsync()).Where(a => a.Category != ResourceCategoryEnum.Module).OrderBy(a => a.Module).ThenBy(a => a.Id).ToList();
|
||||
|
||||
Items = ResourceUtil.BuildTreeItemList(items, Value, RenderTreeItem);
|
||||
Items = AdminResourceUtil.BuildTreeItemList(items, Value, RenderTreeItem);
|
||||
ModuleList = (await SysResourceService.GetAllAsync()).Where(a => a.Category == ResourceCategoryEnum.Module).ToList();
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
@@ -35,7 +35,7 @@ public partial class SysUserEdit
|
||||
BoolItems = LocalizerUtil.GetBoolItems(Model.GetType(), nameof(Model.Status));
|
||||
var items = await SysPositionService.SelectorAsync(new PositionSelectorInput());
|
||||
Items = PositionUtil.BuildCascaderItemList(items);
|
||||
ModuleSelectedItems = ResourceUtil.BuildModuleSelectList((await SysResourceService.GetAllAsync())).ToList();
|
||||
ModuleSelectedItems = AdminResourceUtil.BuildModuleSelectList((await SysResourceService.GetAllAsync())).ToList();
|
||||
await InvokeAsync(StateHasChanged);
|
||||
await base.OnInitializedAsync();
|
||||
}
|
||||
|
@@ -18,6 +18,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
|
||||
|
||||
<!--<UseRazorSourceGenerator>false</UseRazorSourceGenerator>-->
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
|
@@ -14,7 +14,7 @@ namespace ThingsGateway.Admin.Razor;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[ThingsGateway.DependencyInjection.SuppressSniffer]
|
||||
public static class ResourceUtil
|
||||
public static class AdminResourceUtil
|
||||
{
|
||||
/// <summary>
|
||||
/// 构造选择项,ID/TITLE
|
@@ -29,7 +29,7 @@
|
||||
<Target Name="AdminPostPublish" AfterTargets="Publish">
|
||||
<ItemGroup>
|
||||
<!-- setting up the variable for convenience -->
|
||||
<AdminFiles Include="bin\$(Configuration)\$(TargetFramework)\SeedData\**" />
|
||||
<AdminFiles Include="$(OutputPath)\$(TargetFramework)\SeedData\**" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
</PropertyGroup>
|
||||
|
@@ -5,7 +5,8 @@
|
||||
#推送:docker push registry.cn-shenzhen.aliyuncs.com/thingsgateway/thingsgateway
|
||||
|
||||
#aspnetcore9.0环境
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
|
||||
#FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0-noble AS base
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
#默认web
|
||||
@@ -13,6 +14,8 @@ EXPOSE 5000
|
||||
|
||||
# 添加时区环境变量,亚洲,上海
|
||||
ENV TimeZone=Asia/Shanghai
|
||||
# 转发头
|
||||
ENV ASPNETCORE_FORWARDEDHEADERS_ENABLED=true
|
||||
# 使用软连接,并且将时区配置覆盖/etc/timezone
|
||||
RUN ln -snf /usr/share/zoneinfo/$TimeZone /etc/localtime && echo $TimeZone > /etc/timezone
|
||||
|
||||
|
@@ -5,7 +5,8 @@
|
||||
#推送:docker push registry.cn-shenzhen.aliyuncs.com/thingsgateway/thingsgateway_arm64
|
||||
|
||||
#aspnetcore9.0环境
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine-arm64v8 AS base
|
||||
#FROM mcr.microsoft.com/dotnet/aspnet:9.0-alpine-arm64v8 AS base
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:8.0-noble-arm64v8 AS base
|
||||
COPY . /app
|
||||
WORKDIR /app
|
||||
#默认web
|
||||
@@ -13,6 +14,8 @@ EXPOSE 5000
|
||||
|
||||
# 添加时区环境变量,亚洲,上海
|
||||
ENV TimeZone=Asia/Shanghai
|
||||
# 转发头
|
||||
ENV ASPNETCORE_FORWARDEDHEADERS_ENABLED=true
|
||||
# 使用软连接,并且将时区配置覆盖/etc/timezone
|
||||
RUN ln -snf /usr/share/zoneinfo/$TimeZone /etc/localtime && echo $TimeZone > /etc/timezone
|
||||
|
||||
|
@@ -39,7 +39,7 @@
|
||||
<BlazorReconnector @rendermode="new InteractiveServerRenderMode(false)" />
|
||||
|
||||
<script src=@($"_content/BootstrapBlazor/js/bootstrap.blazor.bundle.min.js?v={this.GetType().Assembly.GetName().Version}")></script>
|
||||
<script src=@($"{WebsiteConst.DefaultResourceUrl}js/culture.js?v={this.GetType().Assembly.GetName().Version}")></script>
|
||||
<script src=@($"{WebsiteConst.DefaultResourceUrl}js/localStorageUtil.js?v={this.GetType().Assembly.GetName().Version}")></script>
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
<!-- PWA Service Worker -->
|
||||
<script type="text/javascript">'serviceWorker' in navigator && navigator.serviceWorker.register('./service-worker.js')</script>
|
||||
|
@@ -45,7 +45,7 @@
|
||||
</app>
|
||||
|
||||
<script src="_content/BootstrapBlazor/js/bootstrap.blazor.bundle.min.js"></script>
|
||||
<script src=@($"{WebsiteConst.DefaultResourceUrl}js/culture.js")></script>
|
||||
<script src=@($"{WebsiteConst.DefaultResourceUrl}js/localStorageUtil.js")></script>
|
||||
<script src="_framework/blazor.server.js"></script>
|
||||
|
||||
<!-- PWA Service Worker -->
|
||||
|
@@ -15,6 +15,7 @@ using System.Text;
|
||||
|
||||
using ThingsGateway.Admin.Application;
|
||||
using ThingsGateway.DB;
|
||||
using ThingsGateway.NewLife;
|
||||
using ThingsGateway.NewLife.Log;
|
||||
|
||||
namespace ThingsGateway.AdminServer;
|
||||
@@ -64,7 +65,7 @@ public class Program
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
builder.Host.UseSystemd();
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
if (Runtime.IsLegacyWindows)
|
||||
builder.Logging.ClearProviders(); //去除默认的事件日志提供者,某些情况下会日志输出异常,导致程序崩溃
|
||||
}).ConfigureBuilder(builder =>
|
||||
{
|
||||
|
@@ -183,19 +183,22 @@ public class Startup : AppStartup
|
||||
services.AddScoped<IAuthorizationHandler, BlazorServerAuthenticationHandler>();
|
||||
services.AddScoped<AuthenticationStateProvider, BlazorServerAuthenticationStateProvider>();
|
||||
|
||||
if (!NewLife.Runtime.IsLegacyWindows)
|
||||
{
|
||||
#if NET9_0_OR_GREATER
|
||||
var certificate = X509CertificateLoader.LoadPkcs12FromFile("ThingsGateway.pfx", "ThingsGateway", X509KeyStorageFlags.EphemeralKeySet);
|
||||
var certificate = X509CertificateLoader.LoadPkcs12FromFile("ThingsGateway.pfx", "ThingsGateway", X509KeyStorageFlags.EphemeralKeySet);
|
||||
#else
|
||||
var certificate = new X509Certificate2("ThingsGateway.pfx", "ThingsGateway", X509KeyStorageFlags.EphemeralKeySet);
|
||||
var certificate = new X509Certificate2("ThingsGateway.pfx", "ThingsGateway", X509KeyStorageFlags.EphemeralKeySet);
|
||||
#endif
|
||||
services.AddDataProtection()
|
||||
.PersistKeysToFileSystem(new DirectoryInfo("Keys"))
|
||||
.ProtectKeysWithCertificate(certificate)
|
||||
.UseCryptographicAlgorithms(new AuthenticatedEncryptorConfiguration
|
||||
{
|
||||
EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC,
|
||||
ValidationAlgorithm = ValidationAlgorithm.HMACSHA256
|
||||
});
|
||||
services.AddDataProtection()
|
||||
.PersistKeysToFileSystem(new DirectoryInfo("Keys"))
|
||||
.ProtectKeysWithCertificate(certificate)
|
||||
.UseCryptographicAlgorithms(new AuthenticatedEncryptorConfiguration
|
||||
{
|
||||
EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC,
|
||||
ValidationAlgorithm = ValidationAlgorithm.HMACSHA256
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void Use(IApplicationBuilder applicationBuilder, IWebHostEnvironment env)
|
||||
|
@@ -5,6 +5,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
|
||||
|
||||
</PropertyGroup>
|
||||
|
||||
<!--<Import Project="Admin.targets" Condition=" '$(Configuration)' != 'Debug' " />-->
|
||||
|
@@ -30,9 +30,27 @@ public class ImportPreviewOutputBase
|
||||
/// <summary>
|
||||
/// 返回状态
|
||||
/// </summary>
|
||||
public ConcurrentList<(int Row, bool Success, string? ErrorMessage)> Results { get; set; } = new();
|
||||
public ConcurrentList<ImportPreviewResult> Results { get; set; } = new();
|
||||
}
|
||||
public class ImportPreviewResult
|
||||
{
|
||||
public ImportPreviewResult()
|
||||
{
|
||||
|
||||
}
|
||||
public ImportPreviewResult(int row, bool success, string error)
|
||||
{
|
||||
this.Row = row;
|
||||
this.Success = success;
|
||||
this.ErrorMessage = error;
|
||||
}
|
||||
|
||||
public int Row { get; set; }
|
||||
|
||||
public bool Success { get; set; }
|
||||
|
||||
public string? ErrorMessage { get; set; }
|
||||
}
|
||||
/// <summary>
|
||||
/// 导入预览
|
||||
/// </summary>
|
||||
|
@@ -5,6 +5,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
|
||||
@@ -13,7 +14,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BootstrapBlazor.TableExport" Version="9.2.6" />
|
||||
<PackageReference Include="Yitter.IdGenerator" Version="1.0.14" />
|
||||
<PackageReference Include="BootstrapBlazor" Version="9.9.1" />
|
||||
<PackageReference Include="BootstrapBlazor" Version="9.10.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@@ -9,6 +9,7 @@
|
||||
// ------------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace ThingsGateway.DB;
|
||||
|
||||
@@ -41,4 +42,31 @@ public static class FileExtensions
|
||||
}
|
||||
return fileName;
|
||||
}
|
||||
/// <summary>
|
||||
/// 存储本地文件
|
||||
/// </summary>
|
||||
/// <param name="pPath">存储的第一层目录</param>
|
||||
/// <param name="file"></param>
|
||||
/// <returns>文件全路径</returns>
|
||||
public static async Task<string> StorageLocal(this IFormFile file, string pPath = "imports")
|
||||
{
|
||||
string uploadFileFolder = App.WebHostEnvironment?.WebRootPath ?? "wwwroot"!;//赋值路径
|
||||
var now = CommonUtils.GetSingleId();
|
||||
var filePath = Path.Combine(uploadFileFolder, pPath);
|
||||
if (!Directory.Exists(filePath))//如果不存在就创建文件夹
|
||||
Directory.CreateDirectory(filePath);
|
||||
//var fileSuffix = Path.GetExtension(file.Name).ToLower();// 文件后缀
|
||||
var fileObjectName = $"{now}{file.Name}";//存储后的文件名
|
||||
var fileName = Path.Combine(filePath, fileObjectName);//获取文件全路径
|
||||
fileName = fileName.Replace("\\", "/");//格式化一系
|
||||
//存储文件
|
||||
using (var stream = File.Create(Path.Combine(filePath, fileObjectName)))
|
||||
{
|
||||
await file.CopyToAsync(stream).ConfigureAwait(false);
|
||||
}
|
||||
return fileName;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@@ -53,6 +53,8 @@ public static class QueryPageOptionsExtensions
|
||||
return datas;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static IEnumerable<T> GetQuery<T>(this IEnumerable<T> query, QueryPageOptions option, Func<IEnumerable<T>, IEnumerable<T>>? queryFunc = null, FilterKeyValueAction where = null)
|
||||
{
|
||||
if (queryFunc != null)
|
||||
@@ -123,7 +125,36 @@ 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据查询条件返回QueryData
|
||||
/// </summary>
|
||||
public static QueryData<SelectedItem> GetQueryData<T>(this IEnumerable<T> datas, VirtualizeQueryOption option, Func<IEnumerable<T>, IEnumerable<SelectedItem>> func, FilterKeyValueAction where = null)
|
||||
{
|
||||
var ret = new QueryData<SelectedItem>()
|
||||
{
|
||||
IsSorted = false,
|
||||
IsFiltered = false,
|
||||
IsAdvanceSearch = false,
|
||||
IsSearch = !option.SearchText.IsNullOrWhiteSpace()
|
||||
};
|
||||
|
||||
var items = datas.Skip((option.StartIndex)).Take(option.Count);
|
||||
ret.TotalCount = datas.Count();
|
||||
|
||||
ret.Items = func(items).ToList();
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
@@ -209,16 +209,10 @@ public static class SqlSugarExtensions
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public static async Task<bool> UpdateRangeAsync<T>(this SqlSugarClient db, List<T> updateObjs) where T : class, new()
|
||||
public static Task<int> UpdateSetColumnsTrueAsync<T>(this SqlSugarClient db, Expression<Func<T, T>> columns, Expression<Func<T, bool>> whereExpression) where T : class, new()
|
||||
{
|
||||
return await db.Updateable(updateObjs).ExecuteCommandAsync().ConfigureAwait(false) > 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public static async Task<bool> UpdateSetColumnsTrueAsync<T>(this SqlSugarClient db, Expression<Func<T, T>> columns, Expression<Func<T, bool>> whereExpression) where T : class, new()
|
||||
{
|
||||
return await db.Updateable<T>().SetColumns(columns, appendColumnsByDataFilter: true).Where(whereExpression)
|
||||
.ExecuteCommandAsync().ConfigureAwait(false) > 0;
|
||||
return db.Updateable<T>().SetColumns(columns, appendColumnsByDataFilter: true).Where(whereExpression)
|
||||
.ExecuteCommandAsync();
|
||||
}
|
||||
|
||||
private static IEnumerable<T> Sort<T>(this IEnumerable<T> list, BasePageInput basePageInput)
|
||||
|
13
src/Admin/ThingsGateway.DB/Locales/en-US.json
Normal file
13
src/Admin/ThingsGateway.DB/Locales/en-US.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
|
||||
"ThingsGateway.Admin.Application.BaseDataEntity": {
|
||||
"CreateOrgId": "CreateOrgId"
|
||||
},
|
||||
"ThingsGateway.Admin.Application.BaseEntity": {
|
||||
"CreateTime": "CreateTime",
|
||||
"CreateUser": "CreateUser",
|
||||
"SortCode": "SortCode",
|
||||
"UpdateTime": "UpdateTime",
|
||||
"UpdateUser": "UpdateUser"
|
||||
}
|
||||
}
|
13
src/Admin/ThingsGateway.DB/Locales/zh-CN.json
Normal file
13
src/Admin/ThingsGateway.DB/Locales/zh-CN.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
|
||||
"ThingsGateway.DB.BaseDataEntity": {
|
||||
"CreateOrgId": "创建机构Id"
|
||||
},
|
||||
"ThingsGateway.DB.BaseEntity": {
|
||||
"CreateTime": "创建时间",
|
||||
"CreateUser": "创建人",
|
||||
"SortCode": "排序",
|
||||
"UpdateTime": "更新时间",
|
||||
"UpdateUser": "更新人"
|
||||
}
|
||||
}
|
@@ -5,6 +5,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
|
||||
@@ -18,6 +19,12 @@
|
||||
<None Remove="..\..\..\README.zh-CN.md" Pack="false" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Locales\en-US.json" />
|
||||
<EmbeddedResource Include="Locales\zh-CN.json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!--<PackageReference Include="ThingsGateway.Razor" Version="$(SourceGeneratorVersion)" />-->
|
||||
<!--<ProjectReference Include="..\ThingsGateway.Razor\ThingsGateway.Razor.csproj" />-->
|
||||
|
@@ -554,10 +554,9 @@ public static class App
|
||||
{
|
||||
types = ass.GetTypes();
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
XTrace.Log.Warn($"Error load `{ass.FullName}` assembly.");
|
||||
Console.WriteLine($"Error load `{ass.FullName}` assembly.");
|
||||
XTrace.Log.Warn($"Error load `{ass.FullName}` assembly. : {ex.Message}");
|
||||
}
|
||||
|
||||
return types.Where(u => u.IsPublic && !u.IsDefined(typeof(SuppressSnifferAttribute), false));
|
||||
|
@@ -254,6 +254,8 @@ public static class SpecificationDocumentBuilder
|
||||
/// <param name="configure">自定义配置</param>
|
||||
internal static void BuildGen(SwaggerGenOptions swaggerGenOptions, Action<SwaggerGenOptions> configure = null)
|
||||
{
|
||||
|
||||
|
||||
// 创建分组文档
|
||||
CreateSwaggerDocs(swaggerGenOptions);
|
||||
|
||||
@@ -411,7 +413,7 @@ public static class SpecificationDocumentBuilder
|
||||
// 本地函数
|
||||
static string DefaultSchemaIdSelector(Type modelType)
|
||||
{
|
||||
var modelName = modelType.Name;
|
||||
var modelName = modelType.FullName;
|
||||
|
||||
// 处理泛型类型问题
|
||||
if (modelType.IsConstructedGenericType)
|
||||
|
@@ -5,6 +5,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
|
||||
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -30,7 +31,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.4" />
|
||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="$(NET9Version)" />
|
||||
</ItemGroup>
|
||||
|
||||
|
45
src/Admin/ThingsGateway.NewLife.X/Common/BoundedQueue.cs
Normal file
45
src/Admin/ThingsGateway.NewLife.X/Common/BoundedQueue.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
namespace ThingsGateway.NewLife;
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public class BoundedQueue<T> : IEnumerable<T>
|
||||
{
|
||||
private readonly Queue<T> _queue;
|
||||
private readonly int _capacity;
|
||||
private readonly object _syncRoot = new object();
|
||||
|
||||
public BoundedQueue(int capacity)
|
||||
{
|
||||
if (capacity <= 0) throw new ArgumentOutOfRangeException(nameof(capacity));
|
||||
_capacity = capacity;
|
||||
_queue = new Queue<T>(capacity);
|
||||
}
|
||||
|
||||
public void Enqueue(T item)
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
if (_queue.Count == _capacity)
|
||||
_queue.Dequeue();
|
||||
_queue.Enqueue(item);
|
||||
}
|
||||
}
|
||||
|
||||
public int Count
|
||||
{
|
||||
get { lock (_syncRoot) return _queue.Count; }
|
||||
}
|
||||
|
||||
public IEnumerator<T> GetEnumerator()
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
return new List<T>(_queue).GetEnumerator();
|
||||
}
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
|
@@ -0,0 +1,48 @@
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using ThingsGateway.NewLife.Threading;
|
||||
namespace ThingsGateway.NewLife;
|
||||
|
||||
public class ExpiringDictionary<TKey, TValue> : IDisposable
|
||||
{
|
||||
private readonly ConcurrentDictionary<TKey, TValue> _dict = new();
|
||||
private readonly TimerX _cleanupTimer;
|
||||
|
||||
public ExpiringDictionary(int cleanupInterval = 60000)
|
||||
{
|
||||
_cleanupTimer = new TimerX(Clear, null, cleanupInterval, cleanupInterval) { Async = true };
|
||||
}
|
||||
|
||||
public void TryAdd(TKey key, TValue value)
|
||||
{
|
||||
_dict.TryAdd(key, value);
|
||||
}
|
||||
|
||||
public bool TryGetValue(TKey key, out TValue value)
|
||||
{
|
||||
return _dict.TryGetValue(key, out value);
|
||||
}
|
||||
public TValue GetOrAdd(TKey key, Func<TKey, TValue> func)
|
||||
{
|
||||
return _dict.GetOrAdd(key, func);
|
||||
}
|
||||
public TValue GetOrAdd(TKey key, TValue value)
|
||||
{
|
||||
return _dict.GetOrAdd(key, value);
|
||||
}
|
||||
|
||||
public bool TryRemove(TKey key) => _dict.TryRemove(key, out _);
|
||||
|
||||
public void Clear() => _dict.Clear();
|
||||
|
||||
private void Clear(object? state)
|
||||
{
|
||||
_dict.Clear();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_dict.Clear();
|
||||
_cleanupTimer.Dispose();
|
||||
}
|
||||
}
|
@@ -103,6 +103,54 @@ public static class Runtime
|
||||
|
||||
/// <summary>是否OSX环境</summary>
|
||||
public static Boolean OSX => RuntimeInformation.IsOSPlatform(OSPlatform.OSX);
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
|
||||
public static Boolean? isLegacyWindows;
|
||||
|
||||
/// <summary>
|
||||
/// 判断是否老系统 (Vista/2008/7/2008R2)
|
||||
/// </summary>
|
||||
public static Boolean IsLegacyWindows
|
||||
{
|
||||
get
|
||||
{
|
||||
if (isLegacyWindows != null) return isLegacyWindows.Value;
|
||||
|
||||
if (Windows == false)
|
||||
{
|
||||
isLegacyWindows = false;
|
||||
return isLegacyWindows.Value;
|
||||
}
|
||||
var version = Environment.OSVersion.Version;
|
||||
|
||||
// 如果能拿到真实的 6.x 就直接判断
|
||||
if (version.Major == 6 && version.Minor <= 1)
|
||||
{
|
||||
isLegacyWindows = true;
|
||||
return isLegacyWindows.Value;
|
||||
}
|
||||
if (version.Major < 6)
|
||||
{
|
||||
isLegacyWindows = true;
|
||||
return isLegacyWindows.Value;
|
||||
}
|
||||
|
||||
// 如果拿到的是 10.0(Win8.1 之后有虚拟化问题),用 OSDescription 来兜底
|
||||
var desc = RuntimeInformation.OSDescription;
|
||||
// desc 示例: "Microsoft Windows 6.1.7601" (Win7/2008R2)
|
||||
if (desc.Contains("Windows 6.0") || desc.Contains("Windows 6.1"))
|
||||
{
|
||||
isLegacyWindows = true;
|
||||
return isLegacyWindows.Value;
|
||||
}
|
||||
isLegacyWindows = false;
|
||||
return isLegacyWindows.Value;
|
||||
}
|
||||
|
||||
}
|
||||
#endif
|
||||
|
||||
#else
|
||||
/// <summary>是否Web环境</summary>
|
||||
public static Boolean IsWeb => !String.IsNullOrEmpty(System.Web.HttpRuntime.AppDomainAppId);
|
||||
@@ -115,6 +163,8 @@ public static class Runtime
|
||||
|
||||
/// <summary>是否OSX环境</summary>
|
||||
public static Boolean OSX { get; } = Environment.OSVersion.Platform == PlatformID.MacOSX;
|
||||
|
||||
|
||||
#endif
|
||||
#endregion
|
||||
|
||||
|
@@ -21,6 +21,7 @@ namespace ThingsGateway.NewLife.Json.Extension;
|
||||
/// </summary>
|
||||
public static class SystemTextJsonExtension
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// 默认Json规则(带缩进)
|
||||
/// </summary>
|
||||
@@ -31,37 +32,51 @@ public static class SystemTextJsonExtension
|
||||
/// </summary>
|
||||
public static JsonSerializerOptions NoneIndentedOptions;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 默认Json规则(带缩进)
|
||||
/// </summary>
|
||||
public static JsonSerializerOptions IgnoreNullIndentedOptions;
|
||||
|
||||
/// <summary>
|
||||
/// 默认Json规则(无缩进)
|
||||
/// </summary>
|
||||
public static JsonSerializerOptions IgnoreNullNoneIndentedOptions;
|
||||
|
||||
public static JsonSerializerOptions GetOptions(bool writeIndented, bool ignoreNull)
|
||||
{
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
WriteIndented = writeIndented,
|
||||
DefaultIgnoreCondition = ignoreNull
|
||||
? JsonIgnoreCondition.WhenWritingNull
|
||||
: JsonIgnoreCondition.Never,
|
||||
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
||||
};
|
||||
|
||||
options.Converters.Add(new ByteArrayToNumberArrayConverterSystemTextJson());
|
||||
options.Converters.Add(new JTokenSystemTextJsonConverter());
|
||||
options.Converters.Add(new JValueSystemTextJsonConverter());
|
||||
options.Converters.Add(new JObjectSystemTextJsonConverter());
|
||||
options.Converters.Add(new JArraySystemTextJsonConverter());
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
static SystemTextJsonExtension()
|
||||
{
|
||||
IndentedOptions = new JsonSerializerOptions
|
||||
{
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
WriteIndented = true, // 缩进
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, // 忽略 null
|
||||
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
||||
};
|
||||
// 如有自定义Converter,这里添加
|
||||
// IndentedOptions.Converters.Add(new ByteArrayJsonConverter());
|
||||
IndentedOptions.Converters.Add(new ByteArrayToNumberArrayConverterSystemTextJson());
|
||||
IndentedOptions.Converters.Add(new JTokenSystemTextJsonConverter());
|
||||
IndentedOptions.Converters.Add(new JValueSystemTextJsonConverter());
|
||||
IndentedOptions.Converters.Add(new JObjectSystemTextJsonConverter());
|
||||
IndentedOptions.Converters.Add(new JArraySystemTextJsonConverter());
|
||||
NoneIndentedOptions = new JsonSerializerOptions
|
||||
{
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
WriteIndented = false, // 不缩进
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
||||
};
|
||||
NoneIndentedOptions.Converters.Add(new ByteArrayToNumberArrayConverterSystemTextJson());
|
||||
NoneIndentedOptions.Converters.Add(new JTokenSystemTextJsonConverter());
|
||||
NoneIndentedOptions.Converters.Add(new JValueSystemTextJsonConverter());
|
||||
NoneIndentedOptions.Converters.Add(new JObjectSystemTextJsonConverter());
|
||||
NoneIndentedOptions.Converters.Add(new JArraySystemTextJsonConverter());
|
||||
// NoneIndentedOptions.Converters.Add(new ByteArrayJsonConverter());
|
||||
|
||||
IndentedOptions = GetOptions(true, false);
|
||||
NoneIndentedOptions = GetOptions(false, false);
|
||||
|
||||
IgnoreNullIndentedOptions = GetOptions(true, true);
|
||||
IgnoreNullNoneIndentedOptions = GetOptions(false, true);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 反序列化
|
||||
/// </summary>
|
||||
@@ -96,17 +111,17 @@ public static class SystemTextJsonExtension
|
||||
/// <summary>
|
||||
/// 序列化
|
||||
/// </summary>
|
||||
public static string ToSystemTextJsonString(this object item, bool indented = true)
|
||||
public static string ToSystemTextJsonString(this object item, bool indented = true, bool ignoreNull = true)
|
||||
{
|
||||
return JsonSerializer.Serialize(item, item?.GetType() ?? typeof(object), indented ? IndentedOptions : NoneIndentedOptions);
|
||||
return JsonSerializer.Serialize(item, item?.GetType() ?? typeof(object), ignoreNull ? indented ? IgnoreNullIndentedOptions : IgnoreNullNoneIndentedOptions : indented ? IndentedOptions : NoneIndentedOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 序列化
|
||||
/// </summary>
|
||||
public static byte[] ToSystemTextJsonUtf8Bytes(this object item, bool indented = true)
|
||||
public static byte[] ToSystemTextJsonUtf8Bytes(this object item, bool indented = true, bool ignoreNull = true)
|
||||
{
|
||||
return JsonSerializer.SerializeToUtf8Bytes(item, item.GetType(), indented ? IndentedOptions : NoneIndentedOptions);
|
||||
return JsonSerializer.SerializeToUtf8Bytes(item, item.GetType(), ignoreNull ? indented ? IgnoreNullIndentedOptions : IgnoreNullNoneIndentedOptions : indented ? IndentedOptions : NoneIndentedOptions);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -76,6 +76,65 @@ public static class PathHelper
|
||||
#endregion
|
||||
|
||||
#region 路径操作辅助
|
||||
|
||||
public static string GetRelativePath(string relativeTo, string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(relativeTo))
|
||||
throw new ArgumentNullException(nameof(relativeTo));
|
||||
if (string.IsNullOrEmpty(path))
|
||||
throw new ArgumentNullException(nameof(path));
|
||||
|
||||
// 转为绝对路径
|
||||
relativeTo = Path.GetFullPath(relativeTo);
|
||||
path = Path.GetFullPath(path);
|
||||
|
||||
// 末尾确保有分隔符,便于处理目录
|
||||
if (!relativeTo.EndsWith(Path.DirectorySeparatorChar.ToString()))
|
||||
relativeTo += Path.DirectorySeparatorChar;
|
||||
|
||||
// 相同路径
|
||||
if (string.Equals(relativeTo, path, StringComparison.OrdinalIgnoreCase))
|
||||
return ".";
|
||||
|
||||
// 检查 UNC 或不同盘符
|
||||
bool isUnc = relativeTo.StartsWith(@"\\") && path.StartsWith(@"\\");
|
||||
if (!isUnc)
|
||||
{
|
||||
// 不同盘符直接返回绝对路径
|
||||
if (!string.Equals(Path.GetPathRoot(relativeTo), Path.GetPathRoot(path), StringComparison.OrdinalIgnoreCase))
|
||||
return path;
|
||||
}
|
||||
|
||||
// 找到共同前缀长度
|
||||
int length = Math.Min(relativeTo.Length, path.Length);
|
||||
int lastSeparatorIndex = -1;
|
||||
int i;
|
||||
for (i = 0; i < length; i++)
|
||||
{
|
||||
if (char.ToLowerInvariant(relativeTo[i]) != char.ToLowerInvariant(path[i]))
|
||||
break;
|
||||
if (relativeTo[i] == Path.DirectorySeparatorChar)
|
||||
lastSeparatorIndex = i;
|
||||
}
|
||||
|
||||
// 计算上退的 ".."
|
||||
string up = "";
|
||||
for (int j = lastSeparatorIndex + 1; j < relativeTo.Length; j++)
|
||||
{
|
||||
if (relativeTo[j] == Path.DirectorySeparatorChar)
|
||||
up += ".." + Path.DirectorySeparatorChar;
|
||||
}
|
||||
|
||||
// 获取剩余下行部分
|
||||
string down = path.Substring(lastSeparatorIndex + 1);
|
||||
|
||||
// 拼接结果
|
||||
string result = up + down;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
private static String GetPath(String path, Int32 mode)
|
||||
{
|
||||
// 处理路径分隔符,兼容Windows和Linux
|
||||
|
@@ -552,19 +552,29 @@ public static class Reflect
|
||||
// return false;
|
||||
//}
|
||||
|
||||
|
||||
private static readonly ExpiringDictionary<(MethodInfo, Type, object?), Delegate> _delegateCache = new();
|
||||
|
||||
/// <summary>把一个方法转为泛型委托,便于快速反射调用</summary>
|
||||
/// <typeparam name="TFunc"></typeparam>
|
||||
/// <param name="method"></param>
|
||||
/// <param name="target"></param>
|
||||
/// <returns></returns>
|
||||
public static TFunc? As<TFunc>(this MethodInfo method, Object? target = null)
|
||||
public static TFunc? As<TFunc>(this MethodInfo method, object? target = null)
|
||||
{
|
||||
if (method == null) return default;
|
||||
|
||||
if (target == null)
|
||||
return (TFunc?)(Object?)Delegate.CreateDelegate(typeof(TFunc), method, true);
|
||||
else
|
||||
return (TFunc?)(Object?)Delegate.CreateDelegate(typeof(TFunc), target, method, true);
|
||||
var key = (method, typeof(TFunc), target);
|
||||
|
||||
if (_delegateCache.TryGetValue(key, out var del))
|
||||
return (TFunc)(object)del;
|
||||
|
||||
del = target == null
|
||||
? Delegate.CreateDelegate(typeof(TFunc), method, true)
|
||||
: Delegate.CreateDelegate(typeof(TFunc), target, method, true);
|
||||
|
||||
return (TFunc)(object)_delegateCache.GetOrAdd(key, del);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
@@ -190,7 +190,31 @@ public sealed class Crc32 //: HashAlgorithm
|
||||
crc.Update(stream, count);
|
||||
return crc.Value;
|
||||
}
|
||||
/// <summary>
|
||||
/// 添加Sequence进行校验
|
||||
/// </summary>
|
||||
/// <param name="sequence"></param>
|
||||
/// <returns></returns>
|
||||
public Crc32 Update(ReadOnlySequence<byte> sequence)
|
||||
{
|
||||
foreach (var segment in sequence)
|
||||
{
|
||||
Update(segment.Span);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算校验码 (Sequence)
|
||||
/// </summary>
|
||||
/// <param name="sequence"></param>
|
||||
/// <returns></returns>
|
||||
public static UInt32 Compute(ReadOnlySequence<byte> sequence)
|
||||
{
|
||||
var crc = new Crc32();
|
||||
crc.Update(sequence);
|
||||
return crc.Value;
|
||||
}
|
||||
//#region 抽象实现
|
||||
///// <summary>哈希核心</summary>
|
||||
///// <param name="array"></param>
|
||||
|
@@ -12,6 +12,7 @@
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<SignAssembly>True</SignAssembly>
|
||||
<AssemblyOriginatorKeyFile>newlife.snk</AssemblyOriginatorKeyFile>
|
||||
|
||||
|
||||
|
||||
</PropertyGroup>
|
||||
|
@@ -168,12 +168,13 @@ public class TimerScheduler : ILogFeature
|
||||
_period = 60_000;
|
||||
foreach (var timer in arr)
|
||||
{
|
||||
if (!timer.Calling && CheckTime(timer, now))
|
||||
if ((timer.Reentrant || !timer.Calling) && CheckTime(timer, now))
|
||||
{
|
||||
// 必须在主线程设置状态,否则可能异步线程还没来得及设置开始状态,主线程又开始了新的一轮调度
|
||||
timer.Calling = true;
|
||||
if (timer.IsAsyncTask)
|
||||
Task.Factory.StartNew(ExecuteAsync, timer, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default);
|
||||
ExecuteAsync(timer);
|
||||
//Task.Factory.StartNew(ExecuteAsync, timer, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default);
|
||||
else if (!timer.Async)
|
||||
Execute(timer);
|
||||
else
|
||||
@@ -306,8 +307,23 @@ public class TimerScheduler : ILogFeature
|
||||
return;
|
||||
}
|
||||
|
||||
var func = timer.Method.As<Func<Object?, Task>>(target);
|
||||
await func!(timer.State).ConfigureAwait(false);
|
||||
#if NET6_0_OR_GREATER
|
||||
if (timer.IsValueTask)
|
||||
{
|
||||
var func = timer.Method.As<Func<Object?, ValueTask>>(target);
|
||||
var task = func!(timer.State);
|
||||
if (!task.IsCompleted)
|
||||
await task.ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
#endif
|
||||
{
|
||||
var func = timer.Method.As<Func<Object?, Task>>(target);
|
||||
var task = func!(timer.State);
|
||||
if (!task.IsCompleted)
|
||||
await task.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
}
|
||||
catch (ThreadAbortException) { throw; }
|
||||
catch (ThreadInterruptedException) { throw; }
|
||||
|
@@ -72,7 +72,8 @@ public class TimerX : ITimer, ITimerx, IDisposable
|
||||
|
||||
/// <summary>调用中</summary>
|
||||
public Boolean Calling { get; internal set; }
|
||||
|
||||
/// <summary>可重入</summary>
|
||||
public Boolean Reentrant { get; set; } = false;
|
||||
/// <summary>平均耗时。毫秒</summary>
|
||||
public Int32 Cost { get; internal set; }
|
||||
|
||||
@@ -87,6 +88,8 @@ public class TimerX : ITimer, ITimerx, IDisposable
|
||||
|
||||
private DateTime _AbsolutelyNext;
|
||||
private readonly Cron[]? _crons;
|
||||
|
||||
internal bool IsValueTask { get; }
|
||||
#endregion
|
||||
|
||||
// #region 静态
|
||||
@@ -158,6 +161,29 @@ public class TimerX : ITimer, ITimerx, IDisposable
|
||||
Init(dueTime);
|
||||
}
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
|
||||
/// <summary>实例化一个不可重入的定时器</summary>
|
||||
/// <param name="callback">委托</param>
|
||||
/// <param name="state">用户数据</param>
|
||||
/// <param name="dueTime">多久之后开始。毫秒</param>
|
||||
/// <param name="period">间隔周期。毫秒</param>
|
||||
/// <param name="scheduler">调度器</param>
|
||||
public TimerX(Func<Object, ValueTask> callback, Object? state, Int32 dueTime, Int32 period, String? scheduler = null) : this(callback.Target, callback.Method, state, scheduler)
|
||||
{
|
||||
IsValueTask = true;
|
||||
if (callback == null) throw new ArgumentNullException(nameof(callback));
|
||||
if (dueTime < 0) throw new ArgumentOutOfRangeException(nameof(dueTime));
|
||||
|
||||
IsAsyncTask = true;
|
||||
Async = true;
|
||||
Period = period;
|
||||
|
||||
Init(dueTime);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
/// <summary>实例化一个绝对定时器,指定时刻执行,跟当前时间和SetNext无关</summary>
|
||||
/// <param name="callback">委托</param>
|
||||
/// <param name="state">用户数据</param>
|
||||
@@ -210,6 +236,37 @@ public class TimerX : ITimer, ITimerx, IDisposable
|
||||
Init(ms);
|
||||
}
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
|
||||
/// <summary>实例化一个绝对定时器,指定时刻执行,跟当前时间和SetNext无关</summary>
|
||||
/// <param name="callback">委托</param>
|
||||
/// <param name="state">用户数据</param>
|
||||
/// <param name="startTime">绝对开始时间</param>
|
||||
/// <param name="period">间隔周期。毫秒</param>
|
||||
/// <param name="scheduler">调度器</param>
|
||||
public TimerX(Func<Object, ValueTask> callback, Object? state, DateTime startTime, Int32 period, String? scheduler = null) : this(callback.Target, callback.Method, state, scheduler)
|
||||
{
|
||||
IsValueTask = true;
|
||||
if (callback == null) throw new ArgumentNullException(nameof(callback));
|
||||
if (startTime <= DateTime.MinValue) throw new ArgumentOutOfRangeException(nameof(startTime));
|
||||
if (period <= 0) throw new ArgumentOutOfRangeException(nameof(period));
|
||||
|
||||
IsAsyncTask = true;
|
||||
Async = true;
|
||||
Period = period;
|
||||
Absolutely = true;
|
||||
|
||||
//var now = DateTime.Now;
|
||||
var now = Scheduler.GetNow();
|
||||
var next = startTime;
|
||||
while (next < now) next = next.AddMilliseconds(period);
|
||||
|
||||
var ms = (Int64)(next - now).TotalMilliseconds;
|
||||
_AbsolutelyNext = next;
|
||||
Init(ms);
|
||||
}
|
||||
|
||||
#endif
|
||||
/// <summary>实例化一个Cron定时器</summary>
|
||||
/// <param name="callback">委托</param>
|
||||
/// <param name="state">用户数据</param>
|
||||
@@ -274,6 +331,42 @@ public class TimerX : ITimer, ITimerx, IDisposable
|
||||
//Init(_AbsolutelyNext = _cron.GetNext(DateTime.Now));
|
||||
}
|
||||
|
||||
#if NET6_0_OR_GREATER
|
||||
/// <summary>实例化一个Cron定时器</summary>
|
||||
/// <param name="callback">委托</param>
|
||||
/// <param name="state">用户数据</param>
|
||||
/// <param name="cronExpression">Cron表达式。支持多个表达式,分号分隔</param>
|
||||
/// <param name="scheduler">调度器</param>
|
||||
public TimerX(Func<Object, ValueTask> callback, Object? state, String cronExpression, String? scheduler = null) : this(callback.Target, callback.Method, state, scheduler)
|
||||
{
|
||||
IsValueTask = true;
|
||||
if (callback == null) throw new ArgumentNullException(nameof(callback));
|
||||
if (cronExpression.IsNullOrEmpty()) throw new ArgumentNullException(nameof(cronExpression));
|
||||
|
||||
var list = new List<Cron>();
|
||||
foreach (var item in cronExpression.Split(";"))
|
||||
{
|
||||
var cron = new Cron();
|
||||
if (!cron.Parse(item)) throw new ArgumentException($"Invalid Cron expression[{item}]", nameof(cronExpression));
|
||||
|
||||
list.Add(cron);
|
||||
}
|
||||
_crons = list.ToArray();
|
||||
|
||||
IsAsyncTask = true;
|
||||
Async = true;
|
||||
Absolutely = true;
|
||||
|
||||
//var now = DateTime.Now;
|
||||
var now = Scheduler.GetNow();
|
||||
var next = _crons.Min(e => e.GetNext(now));
|
||||
var ms = (Int64)(next - now).TotalMilliseconds;
|
||||
_AbsolutelyNext = next;
|
||||
Init(ms);
|
||||
//Init(_AbsolutelyNext = _cron.GetNext(DateTime.Now));
|
||||
}
|
||||
#endif
|
||||
|
||||
public bool Disposed { get; private set; }
|
||||
/// <summary>销毁定时器</summary>
|
||||
public void Dispose()
|
||||
|
@@ -4,6 +4,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
|
||||
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@@ -8,8 +8,8 @@
|
||||
// QQ群:605534569
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using ThingsGateway.Common.Extension;
|
||||
using ThingsGateway.NewLife;
|
||||
using ThingsGateway.Razor.Extension;
|
||||
|
||||
namespace ThingsGateway.Razor;
|
||||
|
||||
|
@@ -5,7 +5,7 @@
|
||||
<Step @ref="@step" IsVertical="true">
|
||||
<StepItem Text=@Localizer["First"] Title=@Localizer["Upload"]>
|
||||
<InputUpload ShowDeleteButton="true" @bind-Value=_importFile Accept=".xlsx"></InputUpload>
|
||||
<Button class="mt-2" IsAsync OnClick="() => DeviceImport(_importFile)">@Localizer["Validate"]</Button>
|
||||
<PopConfirmButton IsAsync Color=Color.Warning class="mt-2" OnConfirm=@(SaveDeviceImport)>@Localizer["Import"]</PopConfirmButton>
|
||||
</StepItem>
|
||||
<StepItem Text=@Localizer["Second"] Title=@Localizer["ValidateText"]>
|
||||
|
||||
@@ -41,16 +41,12 @@
|
||||
|
||||
}
|
||||
|
||||
<PopConfirmButton IsAsync IsDisabled=@_importPreviews.Any(it => it.Value.HasError) Color=Color.Warning class="mt-2" OnConfirm=@(SaveDeviceImport)>@Localizer["Import"]</PopConfirmButton>
|
||||
<Button class="mt-2" IsAsync OnClick="() => DeviceImport()">@RazorLocalizer["Close"]</Button>
|
||||
|
||||
@*
|
||||
<Button IsAsync class="mt-2" IsDisabled=@_importPreviews.Any(it => it.Value.HasError) OnClick="() => step.Next()">@Localizer["Next"]</Button> *@
|
||||
|
||||
</div>
|
||||
</StepItem>
|
||||
@* <StepItem Text=@Localizer["Third"] Title=@Localizer["Import"]>
|
||||
<PopConfirmButton IsAsync Color=Color.Warning class="mt-2" OnConfirm=@(SaveDeviceImport)>@Localizer["Import"]</PopConfirmButton>
|
||||
</StepItem> *@
|
||||
|
||||
</Step>
|
||||
@code {
|
||||
[NotNull]
|
||||
|
@@ -24,18 +24,17 @@ public partial class ImportExcel
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
[EditorRequired]
|
||||
public Func<Dictionary<string, ImportPreviewOutputBase>, Task> Import { get; set; }
|
||||
public Func<IBrowserFile, Task<Dictionary<string, ImportPreviewOutputBase>>> Import { get; set; }
|
||||
|
||||
[Inject]
|
||||
[NotNull]
|
||||
private IStringLocalizer<ImportExcel>? Localizer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 预览
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
[EditorRequired]
|
||||
public Func<IBrowserFile, Task<Dictionary<string, ImportPreviewOutputBase>>> Preview { get; set; }
|
||||
[Inject]
|
||||
[NotNull]
|
||||
private IStringLocalizer<ThingsGateway.Razor._Imports>? RazorLocalizer { get; set; }
|
||||
|
||||
|
||||
|
||||
[Inject]
|
||||
[NotNull]
|
||||
@@ -47,13 +46,17 @@ public partial class ImportExcel
|
||||
[CascadingParameter]
|
||||
private Func<Task>? OnCloseAsync { get; set; }
|
||||
|
||||
private async Task DeviceImport(IBrowserFile file)
|
||||
private async Task DeviceImport()
|
||||
{
|
||||
try
|
||||
{
|
||||
_importPreviews.Clear();
|
||||
_importPreviews = await Preview.Invoke(file);
|
||||
await step.Next();
|
||||
await InvokeAsync(async () =>
|
||||
{
|
||||
if (OnCloseAsync != null)
|
||||
await OnCloseAsync();
|
||||
await ToastService.Default();
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -67,16 +70,12 @@ public partial class ImportExcel
|
||||
{
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
await Import.Invoke(_importPreviews);
|
||||
_importPreviews = await Import.Invoke(_importFile);
|
||||
_importFile = null;
|
||||
|
||||
await InvokeAsync(async () =>
|
||||
{
|
||||
if (OnCloseAsync != null)
|
||||
await OnCloseAsync();
|
||||
await ToastService.Default();
|
||||
});
|
||||
});
|
||||
await step.Next();
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@@ -0,0 +1,58 @@
|
||||
@using ThingsGateway.Extension
|
||||
@namespace ThingsGateway.Razor
|
||||
<Button OnClick="() => step.Reset()">@Localizer["Reset"]</Button>
|
||||
<h6 class="my-3 green--text">@Localizer["Tip"] </h6>
|
||||
<Step @ref="@step" IsVertical="true">
|
||||
<StepItem Text=@Localizer["First"] Title=@Localizer["Upload"]>
|
||||
<InputUpload ShowDeleteButton="true" @bind-Value=_importFile Accept=".xlsx"></InputUpload>
|
||||
<Button class="mt-2" IsAsync OnClick="() => DeviceImport(_importFile)">@Localizer["Validate"]</Button>
|
||||
</StepItem>
|
||||
<StepItem Text=@Localizer["Second"] Title=@Localizer["ValidateText"]>
|
||||
|
||||
<div class="overflow-y-auto">
|
||||
|
||||
@foreach (var item in _importPreviews)
|
||||
{
|
||||
<div class="mt-2">
|
||||
@(
|
||||
Localizer["UploadCount", item.Key, item.Value.DataCount]
|
||||
)
|
||||
</div>
|
||||
<div class=@((item.Value.HasError ? "my-2 red--text" : "my-2 green--text"))>
|
||||
@(
|
||||
(item.Value.HasError ? "Error" : "Success")
|
||||
)
|
||||
</div>
|
||||
if (item.Value.HasError)
|
||||
{
|
||||
<div style="height:300px;" class="overflow-y-scroll">
|
||||
<Virtualize Items="item.Value.Results.Where(a => !a.Success).OrderBy(a => a.Row).ToList()" Context="item1" ItemSize="60" OverscanCount=2>
|
||||
<ItemContent>
|
||||
<div class="row g-0">
|
||||
<span class="col mx-2">@item1.Row</span>
|
||||
<span class=@((item1.Success ? "green--text col-auto" : "red--text col-auto"))>
|
||||
<strong>@item1.ErrorMessage</strong>
|
||||
</span>
|
||||
</div>
|
||||
</ItemContent>
|
||||
</Virtualize>
|
||||
</div>
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
<PopConfirmButton IsAsync IsKeepDisabled=@_importPreviews.Any(it => it.Value.HasError) Color=Color.Warning class="mt-2" OnConfirm=@(SaveDeviceImport)>@Localizer["Import"]</PopConfirmButton>
|
||||
|
||||
@*
|
||||
<Button IsAsync class="mt-2" IsDisabled=@_importPreviews.Any(it => it.Value.HasError) OnClick="() => step.Next()">@Localizer["Next"]</Button> *@
|
||||
|
||||
</div>
|
||||
</StepItem>
|
||||
@* <StepItem Text=@Localizer["Third"] Title=@Localizer["Import"]>
|
||||
<PopConfirmButton IsAsync Color=Color.Warning class="mt-2" OnConfirm=@(SaveDeviceImport)>@Localizer["Import"]</PopConfirmButton>
|
||||
</StepItem> *@
|
||||
</Step>
|
||||
@code {
|
||||
[NotNull]
|
||||
Step? step { get; set; }
|
||||
}
|
@@ -0,0 +1,86 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
|
||||
// 此代码版权(除特别声明外的代码)归作者本人Diego所有
|
||||
// 源代码使用协议遵循本仓库的开源协议及附加协议
|
||||
// Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
|
||||
// Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
|
||||
// 使用文档:https://thingsgateway.cn/
|
||||
// QQ群:605534569
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace ThingsGateway.Razor;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public partial class ImportExcelConfirm
|
||||
{
|
||||
private Dictionary<string, ImportPreviewOutputBase> _importPreviews = new();
|
||||
|
||||
/// <summary>
|
||||
/// 导入
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
[EditorRequired]
|
||||
public Func<Dictionary<string, ImportPreviewOutputBase>, Task> Import { get; set; }
|
||||
|
||||
[Inject]
|
||||
[NotNull]
|
||||
private IStringLocalizer<ImportExcelConfirm>? Localizer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 预览
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
[EditorRequired]
|
||||
public Func<IBrowserFile, Task<Dictionary<string, ImportPreviewOutputBase>>> Preview { get; set; }
|
||||
|
||||
[Inject]
|
||||
[NotNull]
|
||||
private ToastService? ToastService { get; set; }
|
||||
|
||||
[Required]
|
||||
private IBrowserFile _importFile { get; set; }
|
||||
|
||||
[CascadingParameter]
|
||||
private Func<Task>? OnCloseAsync { get; set; }
|
||||
|
||||
private async Task DeviceImport(IBrowserFile file)
|
||||
{
|
||||
try
|
||||
{
|
||||
_importPreviews.Clear();
|
||||
_importPreviews = await Preview.Invoke(file);
|
||||
await step.Next();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await ToastService.Warn(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveDeviceImport()
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
await Import.Invoke(_importPreviews);
|
||||
_importFile = null;
|
||||
|
||||
await InvokeAsync(async () =>
|
||||
{
|
||||
if (OnCloseAsync != null)
|
||||
await OnCloseAsync();
|
||||
await ToastService.Default();
|
||||
});
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await InvokeAsync(async () => await ToastService.Warn(ex));
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,9 @@
|
||||
::deep .avatar {
|
||||
border-radius: 1.5rem;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background-color: var(--bs-red);
|
||||
color: #fff;
|
||||
flex: 0 0 auto;
|
||||
font-size: 1rem;
|
||||
}
|
@@ -10,7 +10,7 @@
|
||||
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace ThingsGateway.Common.Extension;
|
||||
namespace ThingsGateway.Razor.Extension;
|
||||
|
||||
/// <summary>
|
||||
/// JSRuntime扩展方法
|
||||
@@ -49,4 +49,28 @@ public static class JSRuntimeExtensions
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static async ValueTask<T> GetLocalStorage<T>(this IJSRuntime jsRuntime, string name)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await jsRuntime.InvokeAsync<T>("getLocalStorage", name).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
public static async ValueTask SetLocalStorage<T>(this IJSRuntime jsRuntime, string name, T data)
|
||||
{
|
||||
try
|
||||
{
|
||||
await jsRuntime.InvokeVoidAsync("setLocalStorage", name, data).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@@ -25,7 +25,8 @@
|
||||
"Success": "Success",
|
||||
"TablesExportButtonExcelText": "Export Excel",
|
||||
"TablesImportButtonExcelText": "Import Excel",
|
||||
"True": "Yes"
|
||||
"True": "Yes",
|
||||
"Info": "Info"
|
||||
},
|
||||
"ThingsGateway.Razor.About": {
|
||||
"Community": "Community",
|
||||
@@ -59,6 +60,19 @@
|
||||
"SearchText": "Search Page"
|
||||
},
|
||||
"ThingsGateway.Razor.ImportExcel": {
|
||||
"First": "Step 1",
|
||||
"Import": "If there are no errors during verification, it will be directly imported into the database",
|
||||
"Next": "Next",
|
||||
"Reset": "Reset",
|
||||
"Second": "Step 2",
|
||||
"Third": "Step 3",
|
||||
"Tip": "When the data volume is large (more than 200,000), the import may take more than 1 minute, please be patient",
|
||||
"Upload": "Upload File",
|
||||
"UploadCount": " Table {0}, import {1} records",
|
||||
"Validate": "Validate",
|
||||
"ValidateText": "Validation Content"
|
||||
},
|
||||
"ThingsGateway.Razor.ImportExcelConfirm": {
|
||||
"First": "Step 1",
|
||||
"Import": "Import",
|
||||
"Next": "Next",
|
||||
@@ -67,7 +81,7 @@
|
||||
"Third": "Step 3",
|
||||
"Tip": "When the data volume is large (more than 200,000), the import may take more than 1 minute, please be patient",
|
||||
"Upload": "Upload File",
|
||||
"UploadCount": " Table {0}, expecting to import {1} records",
|
||||
"UploadCount": " Table {0}, import {1} records",
|
||||
"Validate": "Validate",
|
||||
"ValidateText": "Validation Content"
|
||||
},
|
||||
|
@@ -25,7 +25,8 @@
|
||||
"Success": "成功",
|
||||
"TablesExportButtonExcelText": "导出Excel",
|
||||
"TablesImportButtonExcelText": "导入Excel",
|
||||
"True": "是"
|
||||
"True": "是",
|
||||
"Info": "详情"
|
||||
},
|
||||
"ThingsGateway.Razor.About": {
|
||||
"Community": "社区",
|
||||
@@ -59,6 +60,19 @@
|
||||
"SearchText": "搜索页面"
|
||||
},
|
||||
"ThingsGateway.Razor.ImportExcel": {
|
||||
"First": "第一步",
|
||||
"Import": "若验证无错误,将直接导入数据库",
|
||||
"Next": "下一步",
|
||||
"Reset": "重置",
|
||||
"Second": "第二步",
|
||||
"Third": "第三",
|
||||
"Tip": "数据量较大时(大于20万),所需导入时间可能超过1分钟,请耐心等待",
|
||||
"Upload": "上传文件",
|
||||
"UploadCount": " 表 {0},导入 {1} 条数据",
|
||||
"Validate": "验证",
|
||||
"ValidateText": "验证内容"
|
||||
},
|
||||
"ThingsGateway.Razor.ImportExcelConfirm": {
|
||||
"First": "第一步",
|
||||
"Import": "导入",
|
||||
"Next": "下一步",
|
||||
@@ -67,7 +81,7 @@
|
||||
"Third": "第三",
|
||||
"Tip": "数据量较大时(大于20万),所需导入时间可能超过1分钟,请耐心等待",
|
||||
"Upload": "上传文件",
|
||||
"UploadCount": " 表 {0},预计导入 {1} 条数据",
|
||||
"UploadCount": " 表 {0},导入 {1} 条数据",
|
||||
"Validate": "验证",
|
||||
"ValidateText": "验证内容"
|
||||
},
|
||||
|
@@ -3,6 +3,7 @@
|
||||
<Import Project="..\..\PackNuget.props" />
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0</TargetFrameworks>
|
||||
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BootstrapBlazor.FontAwesome" Version="9.1.0" />
|
||||
|
@@ -1,9 +0,0 @@
|
||||
// 设置 culture
|
||||
function setCultureLocalStorage(culture) {
|
||||
localStorage.setItem("culture", culture);
|
||||
}
|
||||
|
||||
// 获取 culture
|
||||
function getCultureLocalStorage() {
|
||||
return localStorage.getItem("culture");
|
||||
}
|
18
src/Admin/ThingsGateway.Razor/wwwroot/js/localStorageUtil.js
Normal file
18
src/Admin/ThingsGateway.Razor/wwwroot/js/localStorageUtil.js
Normal file
@@ -0,0 +1,18 @@
|
||||
// 设置 culture
|
||||
function setCultureLocalStorage(culture) {
|
||||
localStorage.setItem("culture", culture);
|
||||
}
|
||||
|
||||
// 获取 culture
|
||||
function getCultureLocalStorage() {
|
||||
return localStorage.getItem("culture");
|
||||
}
|
||||
|
||||
function getLocalStorage(name) {
|
||||
return JSON.parse(localStorage.getItem(name)) ?? 0;
|
||||
}
|
||||
function setLocalStorage(name, data) {
|
||||
if (localStorage) {
|
||||
localStorage.setItem(name, JSON.stringify(data));
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
@@ -127,35 +132,43 @@ namespace ThingsGateway.SqlSugar
|
||||
{
|
||||
// 准备多部分表单数据
|
||||
var boundary = "---------------" + DateTime.Now.Ticks.ToString("x");
|
||||
var list = new List<Hashtable>();
|
||||
|
||||
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(tableName));
|
||||
|
||||
// 构建schema信息
|
||||
columns.ForEach(d =>
|
||||
var list = ReflectionInoCacheService.Instance.GetOrCreate($"{key}{dateFormat}List<Hashtable>", () =>
|
||||
{
|
||||
if (d.DataType == "TIMESTAMP")
|
||||
var list = new List<Hashtable>();
|
||||
|
||||
// 构建schema信息
|
||||
columns.ForEach(d =>
|
||||
{
|
||||
list.Add(new Hashtable()
|
||||
if (d.DataType == "TIMESTAMP")
|
||||
{
|
||||
list.Add(new Hashtable()
|
||||
{
|
||||
{ "name", d.DbColumnName },
|
||||
{ "type", d.DataType },
|
||||
{ "pattern", dateFormat}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
list.Add(new Hashtable()
|
||||
}
|
||||
else
|
||||
{
|
||||
list.Add(new Hashtable()
|
||||
{
|
||||
{ "name", d.DbColumnName },
|
||||
{ "type", d.DataType }
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return list;
|
||||
}
|
||||
);
|
||||
|
||||
var schema = JsonConvert.SerializeObject(list);
|
||||
|
||||
// 写入CSV文件
|
||||
@@ -184,8 +197,8 @@ namespace ThingsGateway.SqlSugar
|
||||
"multipart/form-data; boundary=" + boundary);
|
||||
|
||||
// 发送请求并处理响应
|
||||
var httpResponseMessage =
|
||||
await Post(client, tableName, 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);
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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
|
||||
|
@@ -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>();
|
||||
|
@@ -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
|
||||
{
|
||||
|
@@ -5,6 +5,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;net9.0;</TargetFrameworks>
|
||||
@@ -22,18 +23,18 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SqlSugarCore.Dm" Version="8.8.0" />
|
||||
<PackageReference Include="SqlSugarCore.Kdbndp" Version="9.3.7.728" />
|
||||
<PackageReference Include="SqlSugarCore.Dm" Version="8.8.1" />
|
||||
<PackageReference Include="SqlSugarCore.Kdbndp" Version="9.3.7.821" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="7.0.20" />
|
||||
<!--<PackageReference Include="Microsoft.Data.Sqlite" Version="$(NET9Version)" />-->
|
||||
<PackageReference Include="MySqlConnector" Version="2.4.0" />
|
||||
<PackageReference Include="Npgsql" Version="9.0.3" />
|
||||
<PackageReference Include="CsvHelper" Version="33.1.0" />
|
||||
<PackageReference Include="TDengine.Connector" Version="3.1.7" />
|
||||
<PackageReference Include="TDengine.Connector" Version="3.1.8" />
|
||||
<PackageReference Include="Oracle.ManagedDataAccess.Core" Version="23.9.1" />
|
||||
<PackageReference Include="Oscar.Data.SqlClient" Version="4.2.21" />
|
||||
<PackageReference Include="Oscar.Data.SqlClient" Version="4.2.23" />
|
||||
<PackageReference Include="System.Data.Common" Version="4.3.0" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.0" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="6.1.1" />
|
||||
<PackageReference Include="System.Reflection.Emit.Lightweight" Version="4.7.0" />
|
||||
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||
<PackageReference Include="System.Formats.Asn1" Version="8.0.2" />
|
||||
|
@@ -1,11 +1,11 @@
|
||||
<Project>
|
||||
|
||||
<PropertyGroup>
|
||||
<PluginVersion>10.10.6</PluginVersion>
|
||||
<ProPluginVersion>10.10.6</ProPluginVersion>
|
||||
<DefaultVersion>10.10.9</DefaultVersion>
|
||||
<AuthenticationVersion>10.10.1</AuthenticationVersion>
|
||||
<SourceGeneratorVersion>10.10.1</SourceGeneratorVersion>
|
||||
<PluginVersion>10.11.21</PluginVersion>
|
||||
<ProPluginVersion>10.11.21</ProPluginVersion>
|
||||
<DefaultVersion>10.11.21</DefaultVersion>
|
||||
<AuthenticationVersion>10.11.2</AuthenticationVersion>
|
||||
<SourceGeneratorVersion>10.11.2</SourceGeneratorVersion>
|
||||
<NET8Version>8.0.19</NET8Version>
|
||||
<NET9Version>9.0.8</NET9Version>
|
||||
<SatelliteResourceLanguages>zh-Hans;en-US</SatelliteResourceLanguages>
|
||||
@@ -28,7 +28,8 @@
|
||||
<AnalysisModeStyle>None</AnalysisModeStyle>
|
||||
|
||||
<NoWarn>
|
||||
CS8603;CS8618;CS1591;CS8625;CS8602;CS8604;CS8600;CS8601;CS8714;CS8619;CS8629;CS8765;CS8634;CS8621;CS8767;CS8633;CS8620;CS8610;CS8631;CS8605;CS8622;CS8613;NU5100;NU5104;NU1903;NU1902;CA1863;CA1812;CA1805;CA1515;CA1508;CA1819;CA1852;CA5394;CA1822;CA1815;CA1813;CA2000;CA5358;CA5384;CA5400;CA5401;CA1814;CA1835;CA5392;CA5350;CA2100;CA1848;CA1810;CA1513;CA5351;CA1510;CA1512;CA1823;NETSDK1206
|
||||
CS8603;CS8618;CS1591;CS8625;CS8602;CS8604;CS8600;CS8601;CS8714;CS8619;CS8629;CS8765;CS8634;CS8621;CS8767;CS8633;CS8620;CS8610;CS8631;CS8605;CS8622;CS8613;NU5100;NU5104;NU1903;NU1902;CA1863;CA1812;CA1805;CA1515;CA1508;CA1819;CA1852;CA5394;CA1822;CA1815;CA1813;CA2000;CA5358;CA5384;CA5400;CA5401;CA1814;CA1835;CA5392;CA5350;CA2100;CA1848;CA1810;CA1513;CA5351;CA1510;CA1512;CA1823;RCS1102;RCS1194;NETSDK1206
|
||||
|
||||
</NoWarn>
|
||||
<TargetFrameworks>net8.0;</TargetFrameworks>
|
||||
<LangVersion>13.0</LangVersion>
|
||||
|
@@ -82,13 +82,14 @@ public static class CSharpScriptEngineExtension
|
||||
{
|
||||
if (source.IsNullOrEmpty()) return null;
|
||||
var field = $"{CacheKey}-{source}";
|
||||
var exfield = $"{CacheKey}-Exception-{source}";
|
||||
var runScript = Instance.Get<T>(field);
|
||||
if (runScript == null)
|
||||
{
|
||||
lock (m_waiterLock)
|
||||
{
|
||||
runScript = Instance.Get<T>(field);
|
||||
if (runScript == null)
|
||||
var hasValue = Instance.TryGetValue<T>(field, out runScript);
|
||||
if (hasValue == false)
|
||||
{
|
||||
var src = source.Split(Environment.NewLine.ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
|
||||
var _using = new StringBuilder();
|
||||
@@ -111,8 +112,6 @@ public static class CSharpScriptEngineExtension
|
||||
}
|
||||
try
|
||||
{
|
||||
|
||||
|
||||
// 动态加载并执行代码
|
||||
runScript = evaluator.With(eval => eval.IsAssemblyUnloadingEnabled = true).LoadCode<T>(
|
||||
$@"
|
||||
@@ -140,11 +139,22 @@ public static class CSharpScriptEngineExtension
|
||||
string exString = string.Format(CSScriptResource.CSScriptResource.Error1, typeof(T).FullName);
|
||||
throw new(exString);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//如果编译失败,应该不重复编译,避免oom
|
||||
Instance.Set<T>(field, null, TimeSpan.FromHours(1));
|
||||
Instance.Set(exfield, ex, TimeSpan.FromHours(1));
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Instance.SetExpire(field, TimeSpan.FromHours(1));
|
||||
|
||||
Instance.SetExpire(exfield, TimeSpan.FromHours(1));
|
||||
if (runScript == null)
|
||||
{
|
||||
throw (Instance.Get<Exception>(exfield) ?? new Exception("compilation error"));
|
||||
}
|
||||
return runScript;
|
||||
}
|
||||
|
||||
|
@@ -93,30 +93,38 @@ public static class ExpressionEvaluatorExtension
|
||||
public static ReadWriteExpressions GetOrAddScript(string source)
|
||||
{
|
||||
var field = $"{CacheKey}-{source}";
|
||||
var exfield = $"{CacheKey}-Exception-{source}";
|
||||
var runScript = Instance.Get<ReadWriteExpressions>(field);
|
||||
if (runScript == null)
|
||||
{
|
||||
if (!source.Contains("return"))
|
||||
var hasValue = Instance.TryGetValue<ReadWriteExpressions>(field, out runScript);
|
||||
if (!hasValue)
|
||||
{
|
||||
source = $"return {source}";//只判断简单脚本中可省略return字符串
|
||||
}
|
||||
var src = source.Split(Environment.NewLine.ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
|
||||
var _using = new StringBuilder();
|
||||
var _body = new StringBuilder();
|
||||
src.ToList().ForEach(l =>
|
||||
{
|
||||
if (l.StartsWith("using "))
|
||||
|
||||
|
||||
if (!source.Contains("return"))
|
||||
{
|
||||
_using.AppendLine(l);
|
||||
source = $"return {source}";//只判断简单脚本中可省略return字符串
|
||||
}
|
||||
else
|
||||
var src = source.Split(Environment.NewLine.ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
|
||||
var _using = new StringBuilder();
|
||||
var _body = new StringBuilder();
|
||||
src.ToList().ForEach(l =>
|
||||
{
|
||||
_body.AppendLine(l);
|
||||
}
|
||||
});
|
||||
// 动态加载并执行代码
|
||||
runScript = CSScript.Evaluator.With(eval => eval.IsAssemblyUnloadingEnabled = true).LoadCode<ReadWriteExpressions>(
|
||||
$@"
|
||||
if (l.StartsWith("using "))
|
||||
{
|
||||
_using.AppendLine(l);
|
||||
}
|
||||
else
|
||||
{
|
||||
_body.AppendLine(l);
|
||||
}
|
||||
});
|
||||
// 动态加载并执行代码
|
||||
try
|
||||
{
|
||||
runScript = CSScript.Evaluator.With(eval => eval.IsAssemblyUnloadingEnabled = true).LoadCode<ReadWriteExpressions>(
|
||||
$@"
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
@@ -137,9 +145,26 @@ public static class ExpressionEvaluatorExtension
|
||||
}}
|
||||
}}
|
||||
");
|
||||
Instance.Set(field, runScript);
|
||||
Instance.Set(field, runScript);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
//如果编译失败,应该不重复编译,避免oom
|
||||
Instance.Set<ReadWriteExpressions>(field, null, TimeSpan.FromHours(1));
|
||||
Instance.Set(exfield, ex, TimeSpan.FromHours(1));
|
||||
throw;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Instance.SetExpire(field, TimeSpan.FromHours(1));
|
||||
Instance.SetExpire(exfield, TimeSpan.FromHours(1));
|
||||
if (runScript == null)
|
||||
{
|
||||
throw (Instance.Get<Exception>(exfield) ?? new Exception("compilation error"));
|
||||
}
|
||||
return runScript;
|
||||
}
|
||||
|
||||
|
@@ -4,10 +4,11 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;</TargetFrameworks>
|
||||
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CS-Script" Version="4.10.1" />
|
||||
<PackageReference Include="CS-Script" Version="4.11.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@@ -89,7 +89,7 @@ public partial class ChannelComponent : ComponentBase
|
||||
await Channel.SetupAsync(config);
|
||||
}
|
||||
|
||||
await Channel.ConnectAsync(Channel.ChannelOptions.ConnectTimeout, default);
|
||||
await Channel.ConnectAsync(default);
|
||||
|
||||
if (OnConnectClick.HasDelegate)
|
||||
await OnConnectClick.InvokeAsync(Channel);
|
||||
|
@@ -23,7 +23,7 @@
|
||||
<EditorItem @bind-Field=Model.EncodingName>
|
||||
<EditTemplate Context="value">
|
||||
<div class="col-12 col-sm-4">
|
||||
<Select @bind-Value=value.EncodingName Items="EncodingItems" />
|
||||
<Select @bind-Value=value.EncodingName Items="EncodingItems" IsClearable/>
|
||||
</div>
|
||||
</EditTemplate>
|
||||
</EditorItem>
|
||||
|
@@ -31,6 +31,6 @@ public partial class ConverterConfigComponent : ComponentBase
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
BoolItems = LocalizerUtil.GetBoolItems(Model.GetType(), nameof(Model.VariableStringLength), true);
|
||||
EncodingItems = new List<SelectedItem>() { new SelectedItem("", "none") }.Concat(Encoding.GetEncodings().Select(a => new SelectedItem(a.CodePage.ToString(), a.DisplayName))).ToList();
|
||||
EncodingItems = Encoding.GetEncodings().Select(a => new SelectedItem(a.CodePage.ToString(), a.DisplayName)).ToList();
|
||||
}
|
||||
}
|
||||
|
@@ -95,7 +95,7 @@ public partial class LogConsole : IDisposable
|
||||
|
||||
if (LogPath != null)
|
||||
{
|
||||
var files = await TextFileReadService.GetLogFiles(LogPath);
|
||||
var files = await TextFileReadService.GetLogFilesAsync(LogPath);
|
||||
if (!files.IsSuccess)
|
||||
{
|
||||
Messages = new List<LogMessage>();
|
||||
@@ -106,7 +106,7 @@ public partial class LogConsole : IDisposable
|
||||
await Task.Run(async () =>
|
||||
{
|
||||
Stopwatch sw = Stopwatch.StartNew();
|
||||
var result = await TextFileReadService.LastLogData(files.Content.FirstOrDefault());
|
||||
var result = await TextFileReadService.LastLogDataAsync(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();
|
||||
@@ -144,7 +144,7 @@ public partial class LogConsole : IDisposable
|
||||
{
|
||||
if (LogPath != null)
|
||||
{
|
||||
var files = await TextFileReadService.GetLogFiles(LogPath);
|
||||
var files = await TextFileReadService.GetLogFilesAsync(LogPath);
|
||||
if (files.IsSuccess)
|
||||
{
|
||||
foreach (var item in files.Content)
|
||||
|
@@ -8,7 +8,7 @@
|
||||
// QQ群:605534569
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace ThingsGateway.Gateway.Razor;
|
||||
namespace ThingsGateway.Debug;
|
||||
|
||||
public class ValueTransformConfig
|
||||
{
|
@@ -1,9 +1,5 @@
|
||||
@namespace ThingsGateway.Gateway.Razor
|
||||
@using ThingsGateway.Admin.Application
|
||||
@using ThingsGateway.Admin.Razor
|
||||
@namespace ThingsGateway.Debug
|
||||
@using ThingsGateway.Foundation
|
||||
@using ThingsGateway.Gateway.Application
|
||||
@inherits ComponentDefault
|
||||
|
||||
<ValidateForm class="p-4 h-100" Model="@ValueTransformConfig" OnValidSubmit="OnSave">
|
||||
<EditorForm AutoGenerateAllItem="false" RowType=RowType.Inline ItemsPerRow=1 LabelWidth=150 Model="ValueTransformConfig">
|
@@ -14,7 +14,7 @@ using System.Text.RegularExpressions;
|
||||
|
||||
using ThingsGateway.NewLife.Extension;
|
||||
|
||||
namespace ThingsGateway.Gateway.Razor;
|
||||
namespace ThingsGateway.Debug;
|
||||
|
||||
public partial class ValueTransformConfigPage
|
||||
{
|
||||
@@ -205,5 +205,10 @@ public partial class ValueTransformConfigPage
|
||||
}
|
||||
}
|
||||
|
||||
[Inject]
|
||||
ToastService ToastService { get; set; }
|
||||
|
||||
[Inject]
|
||||
IStringLocalizer<ThingsGateway.Razor._Imports> RazorLocalizer { get; set; }
|
||||
#endregion 修改
|
||||
}
|
@@ -1,4 +1,23 @@
|
||||
{
|
||||
|
||||
|
||||
"ThingsGateway.Debug.ValueTransformType": {
|
||||
"None": "None",
|
||||
"Linear": "Linear",
|
||||
"Sqrt": "Sqrt"
|
||||
},
|
||||
|
||||
"ThingsGateway.Debug.ValueTransformConfig": {
|
||||
"TransformType": "TransformType",
|
||||
"MinMax": "MinMax",
|
||||
"ClampToRawRange": "ClampToRawRange",
|
||||
"DecimalPlaces": "DecimalPlaces",
|
||||
"RawMin": "RawMin",
|
||||
"RawMax": "RawMax",
|
||||
"ActualMin": "ActualMin",
|
||||
"ActualMax": "ActualMax"
|
||||
},
|
||||
|
||||
"ThingsGateway.Debug.ChannelComponent": {
|
||||
"BaudRate": "Baud Rate",
|
||||
"BindUrl": "Local Bind IP Address",
|
||||
|
@@ -1,4 +1,21 @@
|
||||
{
|
||||
|
||||
"ThingsGateway.Debug.ValueTransformType": {
|
||||
"None": "无",
|
||||
"Linear": "线性",
|
||||
"Sqrt": "开方"
|
||||
},
|
||||
"ThingsGateway.Debug.ValueTransformConfig": {
|
||||
"TransformType": "转换方式",
|
||||
"MinMax": "最小最大值",
|
||||
"ClampToRawRange": "限制范围",
|
||||
"DecimalPlaces": "保留小数位",
|
||||
"RawMin": "原始最小值",
|
||||
"RawMax": "原始最大值",
|
||||
"ActualMin": "实际最小值",
|
||||
"ActualMax": "实际最大值"
|
||||
},
|
||||
|
||||
"ThingsGateway.Debug.ChannelComponent": {
|
||||
"BaudRate": "波特率",
|
||||
"BindUrl": "本地url",
|
||||
|
@@ -40,7 +40,7 @@ public class PlatformService : IPlatformService
|
||||
await using var jSObject = await JSRuntime.InvokeAsync<IJSObjectReference>("import", $"{WebsiteConst.DefaultResourceUrl}js/downloadFile.js");
|
||||
var path = Path.GetRelativePath("wwwroot", item);
|
||||
string fileName = DateTime.Now.ToFileDateTimeFormat();
|
||||
await jSObject.InvokeVoidAsync("blazor_downloadFile", url, fileName, new { FileName = path });
|
||||
await jSObject.InvokeAsync<bool>("blazor_downloadFile", url, fileName, new { FileName = path });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@
|
||||
<Import Project="..\..\PackNuget.props" />
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;</TargetFrameworks>
|
||||
|
||||
<!--<UseRazorSourceGenerator>false</UseRazorSourceGenerator>-->
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
|
@@ -5,6 +5,7 @@
|
||||
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||
<NoPackageAnalysis>true</NoPackageAnalysis>
|
||||
<SignAssembly>false</SignAssembly>
|
||||
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@@ -5,6 +5,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;</TargetFrameworks>
|
||||
<Version>$(SourceGeneratorVersion)</Version>
|
||||
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@@ -8,6 +8,8 @@
|
||||
// QQ群:605534569
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using System.Buffers;
|
||||
|
||||
namespace ThingsGateway.Foundation;
|
||||
|
||||
/// <summary>
|
||||
@@ -16,12 +18,12 @@ namespace ThingsGateway.Foundation;
|
||||
public abstract class DDPMessage : MessageBase, IResultMessage
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override int HeaderLength => 4;
|
||||
public override long HeaderLength => 4;
|
||||
public byte Type = 0;
|
||||
public string Id;
|
||||
public override FilterResult CheckBody<TByteBlock>(ref TByteBlock byteBlock)
|
||||
{
|
||||
Id = byteBlock.ToString(byteBlock.Position, 11).Replace("\0", "");
|
||||
Id = byteBlock.ToString(byteBlock.BytesRead, 11).Replace("\0", "");
|
||||
OperCode = 0;
|
||||
|
||||
Content = GetContent(ref byteBlock);
|
||||
@@ -44,31 +46,31 @@ public abstract class DDPMessage : MessageBase, IResultMessage
|
||||
}
|
||||
}
|
||||
|
||||
public abstract int GetBodyLength<TByteBlock>(ref TByteBlock byteBlock) where TByteBlock : IByteBlockReader;
|
||||
public abstract byte[] GetContent<TByteBlock>(ref TByteBlock byteBlock) where TByteBlock : IByteBlockReader;
|
||||
public abstract long GetBodyLength<TByteBlock>(ref TByteBlock byteBlock) where TByteBlock : IBytesReader;
|
||||
public abstract byte[] GetContent<TByteBlock>(ref TByteBlock byteBlock) where TByteBlock : IBytesReader;
|
||||
}
|
||||
|
||||
public class DDPTcpMessage : DDPMessage
|
||||
{
|
||||
public override int GetBodyLength<TByteBlock>(ref TByteBlock byteBlock)
|
||||
public override long GetBodyLength<TByteBlock>(ref TByteBlock byteBlock)
|
||||
{
|
||||
return ReaderExtension.ReadValue<TByteBlock, ushort>(ref byteBlock, EndianType.Big) - 4;
|
||||
}
|
||||
|
||||
public override byte[] GetContent<TByteBlock>(ref TByteBlock byteBlock)
|
||||
{
|
||||
return byteBlock.Span.Slice(byteBlock.Position + 11, BodyLength - 12).ToArray();
|
||||
return byteBlock.TotalSequence.Slice(byteBlock.BytesRead + 11, BodyLength - 12).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public class DDPUdpMessage : DDPMessage
|
||||
{
|
||||
public override int GetBodyLength<TByteBlock>(ref TByteBlock byteBlock)
|
||||
public override long GetBodyLength<TByteBlock>(ref TByteBlock byteBlock)
|
||||
{
|
||||
return byteBlock.Length - 4;
|
||||
return (byteBlock.BytesRead + byteBlock.BytesRemaining - 4);
|
||||
}
|
||||
public override byte[] GetContent<TByteBlock>(ref TByteBlock byteBlock)
|
||||
{
|
||||
return byteBlock.Span.Slice(byteBlock.Position + 12, BodyLength - 12).ToArray();
|
||||
return byteBlock.TotalSequence.Slice(byteBlock.BytesRead + 12, BodyLength - 12).ToArray();
|
||||
}
|
||||
}
|
@@ -25,6 +25,7 @@ public class DDPSend : ISendMessage
|
||||
string Id;
|
||||
byte Command;
|
||||
bool Tcp;
|
||||
|
||||
public DDPSend(ReadOnlyMemory<byte> readOnlyMemory, string id, bool tcp, byte command = 0x89)
|
||||
{
|
||||
Tcp = tcp;
|
||||
@@ -32,7 +33,8 @@ public class DDPSend : ISendMessage
|
||||
Id = id;
|
||||
Command = command;
|
||||
}
|
||||
public void Build<TByteBlock>(ref TByteBlock byteBlock) where TByteBlock : IByteBlockWriter
|
||||
|
||||
public void Build<TByteBlock>(ref TByteBlock byteBlock) where TByteBlock : IBytesWriter
|
||||
{
|
||||
WriterExtension.WriteValue(ref byteBlock, (byte)0x7b);
|
||||
WriterExtension.WriteValue(ref byteBlock, (byte)Command);
|
||||
|
@@ -10,8 +10,6 @@
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
using ThingsGateway.NewLife;
|
||||
|
||||
using TouchSocket.Resources;
|
||||
|
||||
namespace ThingsGateway.Foundation;
|
||||
@@ -33,50 +31,120 @@ public class DDPTcpSessionClientChannel : TcpSessionClientChannel
|
||||
DDPAdapter.Config(Config);
|
||||
}
|
||||
|
||||
// 将当前实例的日志记录器和加载回调设置到适配器中
|
||||
DDPAdapter.Logger = Logger;
|
||||
DDPAdapter.OnLoaded(this);
|
||||
|
||||
DDPAdapter.SendAsyncCallBack = DDPSendAsync;
|
||||
DDPAdapter.ReceivedAsyncCallBack = DDPHandleReceivedData;
|
||||
DataHandlingAdapter.SendAsyncCallBack = DefaultSendAsync;
|
||||
return base.OnTcpConnected(e);
|
||||
}
|
||||
protected Task DefaultSendAsync(ReadOnlyMemory<byte> memory, CancellationToken cancellationToken)
|
||||
{
|
||||
return DDPAdapter.SendInputAsync(new DDPSend(memory, Id, true), cancellationToken);
|
||||
}
|
||||
protected Task DDPSendAsync(ReadOnlyMemory<byte> memory, CancellationToken cancellationToken)
|
||||
{
|
||||
return base.ProtectedDefaultSendAsync(memory, cancellationToken);
|
||||
}
|
||||
|
||||
private DDPMessage DDPMessage { get; set; }
|
||||
private Task DDPHandleReceivedData(IByteBlockReader byteBlock, IRequestInfo requestInfo)
|
||||
#region 发送
|
||||
|
||||
/// <summary>
|
||||
/// 异步发送数据,通过适配器模式灵活处理数据发送。
|
||||
/// </summary>
|
||||
/// <param name="memory">待发送的只读字节内存块。</param>
|
||||
/// <param name="token">可取消令箭</param>
|
||||
/// <returns>一个异步任务,表示发送操作。</returns>
|
||||
protected virtual async Task NewProtectedSendAsync(ReadOnlyMemory<byte> memory, CancellationToken token)
|
||||
{
|
||||
if (requestInfo is DDPMessage dDPMessage)
|
||||
DDPMessage = dDPMessage;
|
||||
this.ThrowIfDisposed();
|
||||
this.ThrowIfClientNotConnected();
|
||||
|
||||
return EasyTask.CompletedTask;
|
||||
}
|
||||
|
||||
private DeviceSingleStreamDataHandleAdapter<DDPTcpMessage> DDPAdapter = new();
|
||||
private WaitLock _waitLock = new(nameof(DDPTcpSessionClientChannel));
|
||||
if (!await this.OnTcpSending(memory).ConfigureAwait(false)) return;
|
||||
|
||||
protected override async ValueTask<bool> OnTcpReceiving(IByteBlockReader byteBlock)
|
||||
{
|
||||
DDPMessage? message = null;
|
||||
var transport = this.Transport;
|
||||
var adapter = this.DataHandlingAdapter;
|
||||
var locker = transport.SemaphoreSlimForWriter;
|
||||
|
||||
await locker.WaitAsync(token).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await _waitLock.WaitAsync().ConfigureAwait(false);
|
||||
await DDPAdapter.ReceivedInputAsync(byteBlock).ConfigureAwait(false);
|
||||
|
||||
message = DDPMessage;
|
||||
DDPMessage = null;
|
||||
// 如果数据处理适配器未设置,则使用默认发送方式。
|
||||
if (adapter == null)
|
||||
{
|
||||
await transport.Output.WriteAsync(memory, token).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
var byteBlock = new ByteBlock(1024);
|
||||
var ddpSend = new DDPSend(memory, Id, true);
|
||||
ddpSend.Build(ref byteBlock);
|
||||
var newMemory = byteBlock.Memory;
|
||||
var writer = new PipeBytesWriter(transport.Output);
|
||||
adapter.SendInput(ref writer, in newMemory);
|
||||
await writer.FlushAsync(token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_waitLock.Release();
|
||||
locker.Release();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步发送请求信息的受保护方法。
|
||||
///
|
||||
/// 此方法首先检查当前对象是否能够发送请求信息,如果不能,则抛出异常。
|
||||
/// 如果可以发送,它将使用数据处理适配器来异步发送输入请求。
|
||||
/// </summary>
|
||||
/// <param name="requestInfo">要发送的请求信息。</param>
|
||||
/// <param name="token">可取消令箭</param>
|
||||
/// <returns>返回一个任务,该任务代表异步操作的结果。</returns>
|
||||
protected virtual async Task NewProtectedSendAsync(IRequestInfo requestInfo, CancellationToken token)
|
||||
{
|
||||
// 检查是否具备发送请求的条件,如果不具备则抛出异常
|
||||
this.ThrowIfCannotSendRequestInfo();
|
||||
|
||||
this.ThrowIfDisposed();
|
||||
this.ThrowIfClientNotConnected();
|
||||
|
||||
var transport = this.Transport;
|
||||
var adapter = this.DataHandlingAdapter;
|
||||
var locker = transport.SemaphoreSlimForWriter;
|
||||
|
||||
await locker.WaitAsync(token).ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var byteBlock = new ByteBlock(1024);
|
||||
if (requestInfo is not IRequestInfoBuilder requestInfoBuilder)
|
||||
{
|
||||
throw new Exception();
|
||||
}
|
||||
requestInfoBuilder.Build(ref byteBlock);
|
||||
var ddpSend = new DDPSend(byteBlock.Memory, Id, true);
|
||||
|
||||
var writer = new PipeBytesWriter(transport.Output);
|
||||
adapter.SendInput(ref writer, ddpSend);
|
||||
await writer.FlushAsync(token).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
locker.Release();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#endregion 发送
|
||||
public override Task SendAsync(IRequestInfo requestInfo, CancellationToken token = default)
|
||||
{
|
||||
return NewProtectedSendAsync(requestInfo, token);
|
||||
}
|
||||
|
||||
public override Task SendAsync(ReadOnlyMemory<byte> memory, CancellationToken token = default)
|
||||
{
|
||||
return NewProtectedSendAsync(memory, token);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private DeviceSingleStreamDataHandleAdapter<DDPTcpMessage> DDPAdapter = new();
|
||||
|
||||
protected override async ValueTask<bool> OnTcpReceiving(IBytesReader byteBlock)
|
||||
{
|
||||
|
||||
if (DDPAdapter.TryParseRequest(ref byteBlock, out var message))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message != null)
|
||||
@@ -90,11 +158,11 @@ public class DDPTcpSessionClientChannel : TcpSessionClientChannel
|
||||
|
||||
if (this.DataHandlingAdapter == null)
|
||||
{
|
||||
await this.OnTcpReceived(new ReceivedDataEventArgs(reader, default)).ConfigureAwait(EasyTask.ContinueOnCapturedContext);
|
||||
await this.OnTcpReceived(new ReceivedDataEventArgs(message.Content, default)).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await this.DataHandlingAdapter.ReceivedInputAsync(reader).ConfigureAwait(EasyTask.ContinueOnCapturedContext);
|
||||
await this.DataHandlingAdapter.ReceivedInputAsync(reader).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -127,16 +195,16 @@ public class DDPTcpSessionClientChannel : TcpSessionClientChannel
|
||||
}
|
||||
}
|
||||
|
||||
await ResetIdAsync(id).ConfigureAwait(false);
|
||||
await ResetIdAsync(id, ClosedToken).ConfigureAwait(false);
|
||||
|
||||
//发送成功
|
||||
await DDPAdapter.SendInputAsync(new DDPSend(ReadOnlyMemory<byte>.Empty, id, true, 0x81), ClosedToken).ConfigureAwait(false);
|
||||
await base.ProtectedSendAsync(new DDPSend(ReadOnlyMemory<byte>.Empty, id, true, 0x81), ClosedToken).ConfigureAwait(false);
|
||||
if (log)
|
||||
Logger?.Info(string.Format(AppResource.DtuConnected, Id));
|
||||
}
|
||||
else if (message.Type == 0x02)
|
||||
{
|
||||
await DDPAdapter.SendInputAsync(new DDPSend(ReadOnlyMemory<byte>.Empty, Id, true, 0x82), ClosedToken).ConfigureAwait(false);
|
||||
await base.ProtectedSendAsync(new DDPSend(ReadOnlyMemory<byte>.Empty, Id, true, 0x82), ClosedToken).ConfigureAwait(false);
|
||||
Logger?.Info(string.Format(AppResource.DtuDisconnecting, Id));
|
||||
await Task.Delay(100).ConfigureAwait(false);
|
||||
await this.CloseAsync().ConfigureAwait(false);
|
||||
|
@@ -36,16 +36,12 @@ public class DDPUdpSessionChannel : UdpSessionChannel, IClientChannel, IDtuUdpSe
|
||||
DDPAdapter.Config(Config);
|
||||
}
|
||||
|
||||
// 将当前实例的日志记录器和加载回调设置到适配器中
|
||||
DDPAdapter.Logger = Logger;
|
||||
|
||||
if (DDPAdapter.Owner != null)
|
||||
{
|
||||
DDPAdapter.OnLoaded(this);
|
||||
}
|
||||
|
||||
DDPAdapter.SendCallBackAsync = DDPSendAsync;
|
||||
DDPAdapter.ReceivedCallBack = DDPHandleReceivedData;
|
||||
DDPAdapter.SendCallBackAsync = base.ProtectedDefaultSendAsync;
|
||||
DataHandlingAdapter.SendCallBackAsync = DefaultSendAsync;
|
||||
}
|
||||
|
||||
@@ -62,22 +58,7 @@ public class DDPUdpSessionChannel : UdpSessionChannel, IClientChannel, IDtuUdpSe
|
||||
}
|
||||
}
|
||||
|
||||
protected Task DDPSendAsync(EndPoint endPoint, ReadOnlyMemory<byte> memory, CancellationToken token)
|
||||
{
|
||||
//获取endpoint
|
||||
return base.ProtectedDefaultSendAsync(endPoint, memory, token);
|
||||
}
|
||||
|
||||
private ConcurrentDictionary<EndPoint, DDPMessage> DDPMessageDict { get; set; } = new();
|
||||
private Task DDPHandleReceivedData(EndPoint endPoint, IByteBlockReader byteBlock, IRequestInfo requestInfo)
|
||||
{
|
||||
if (requestInfo is DDPMessage dDPMessage)
|
||||
{
|
||||
DDPMessageDict.AddOrUpdate(endPoint, dDPMessage);
|
||||
}
|
||||
|
||||
return EasyTask.CompletedTask;
|
||||
}
|
||||
|
||||
private DeviceUdpDataHandleAdapter<DDPUdpMessage> DDPAdapter = new();
|
||||
|
||||
@@ -98,27 +79,14 @@ public class DDPUdpSessionChannel : UdpSessionChannel, IClientChannel, IDtuUdpSe
|
||||
return base.StopAsync(token);
|
||||
}
|
||||
|
||||
private ConcurrentDictionary<EndPoint, WaitLock> _waitLocks = new();
|
||||
|
||||
protected override async ValueTask<bool> OnUdpReceiving(UdpReceiveingEventArgs e)
|
||||
{
|
||||
var byteBlock = e.ByteBlock;
|
||||
var byteBlock = e.Memory;
|
||||
var endPoint = e.EndPoint;
|
||||
DDPMessage? message = null;
|
||||
var waitLock = _waitLocks.GetOrAdd(endPoint, new WaitLock(nameof(DDPUdpSessionChannel)));
|
||||
try
|
||||
{
|
||||
await waitLock.WaitAsync().ConfigureAwait(false);
|
||||
await DDPAdapter.ReceivedInput(endPoint, byteBlock).ConfigureAwait(EasyTask.ContinueOnCapturedContext);
|
||||
|
||||
if (DDPMessageDict.TryGetValue(endPoint, out var dDPMessage))
|
||||
message = dDPMessage;
|
||||
DDPMessageDict.TryRemove(endPoint, out _);
|
||||
}
|
||||
finally
|
||||
{
|
||||
waitLock.Release();
|
||||
}
|
||||
if (!DDPAdapter.TryParseRequest(endPoint, byteBlock, out var message))
|
||||
return true;
|
||||
|
||||
if (message != null)
|
||||
{
|
||||
@@ -127,15 +95,13 @@ public class DDPUdpSessionChannel : UdpSessionChannel, IClientChannel, IDtuUdpSe
|
||||
var id = $"ID={message.Id}";
|
||||
if (message.Type == 0x09)
|
||||
{
|
||||
var reader = new ByteBlockReader(message.Content);
|
||||
|
||||
if (this.DataHandlingAdapter == null)
|
||||
{
|
||||
await this.OnUdpReceived(new UdpReceivedDataEventArgs(endPoint, reader, default)).ConfigureAwait(EasyTask.ContinueOnCapturedContext);
|
||||
await this.OnUdpReceived(new UdpReceivedDataEventArgs(endPoint, message.Content, default)).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await this.DataHandlingAdapter.ReceivedInput(endPoint, reader).ConfigureAwait(EasyTask.ContinueOnCapturedContext);
|
||||
await this.DataHandlingAdapter.ReceivedInputAsync(endPoint, message.Content).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
@@ -37,7 +37,9 @@ public static class ChannelOptionsExtensions
|
||||
for (int i = 0; i < funcs.Count; i++)
|
||||
{
|
||||
var func = funcs[i];
|
||||
await func.Invoke(clientChannel, e, i == funcs.Count - 1).ConfigureAwait(false);
|
||||
var task = func.Invoke(clientChannel, e, i == funcs.Count - 1);
|
||||
if (!task.IsCompleted)
|
||||
await task.ConfigureAwait(false);
|
||||
if (e.Handled)
|
||||
{
|
||||
break;
|
||||
|
@@ -8,8 +8,6 @@
|
||||
// QQ群:605534569
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace ThingsGateway.Foundation;
|
||||
|
||||
/// <summary>
|
||||
@@ -59,15 +57,9 @@ public interface IChannel : ISetupConfigObject, IDisposable, IClosableClient, IC
|
||||
/// </summary>
|
||||
public ChannelEventHandler Stoping { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 主动请求时的等待池
|
||||
/// </summary>
|
||||
public ConcurrentDictionary<long, Func<IClientChannel, ReceivedDataEventArgs, bool, Task>> ChannelReceivedWaitDict { get; }
|
||||
|
||||
void ResetSign(int minSign = 0, int maxSign = ushort.MaxValue);
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@@ -22,6 +22,8 @@ public interface IClientChannel : IChannel, ISender, IClient, IClientSender, IOn
|
||||
/// </summary>
|
||||
DataHandlingAdapter ReadOnlyDataHandlingAdapter { get; }
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 通道等待池
|
||||
/// </summary>
|
||||
@@ -34,4 +36,6 @@ public interface IClientChannel : IChannel, ISender, IClient, IClientSender, IOn
|
||||
/// </summary>
|
||||
/// <param name="adapter">适配器</param>
|
||||
void SetDataHandlingAdapter(DataHandlingAdapter adapter);
|
||||
|
||||
void SetDataHandlingAdapterLogger(ILog log);
|
||||
}
|
||||
|
@@ -8,8 +8,6 @@
|
||||
// QQ群:605534569
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using ThingsGateway.NewLife;
|
||||
|
||||
using TouchSocket.SerialPorts;
|
||||
@@ -31,13 +29,23 @@ public class OtherChannel : SetupConfigObject, IClientChannel
|
||||
}
|
||||
|
||||
public override TouchSocketConfig Config => base.Config ?? ChannelOptions.Config;
|
||||
public void SetDataHandlingAdapterLogger(ILog log)
|
||||
{
|
||||
if (_deviceDataHandleAdapter != ReadOnlyDataHandlingAdapter && ReadOnlyDataHandlingAdapter is IDeviceDataHandleAdapter handleAdapter)
|
||||
{
|
||||
_deviceDataHandleAdapter = handleAdapter;
|
||||
}
|
||||
if (_deviceDataHandleAdapter != null)
|
||||
{
|
||||
_deviceDataHandleAdapter.Logger = log;
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetSign(int minSign = 0, int maxSign = ushort.MaxValue)
|
||||
{
|
||||
var pool = WaitHandlePool;
|
||||
WaitHandlePool = new WaitHandlePool<MessageBase>(minSign, maxSign);
|
||||
pool?.CancelAll();
|
||||
pool?.SafeDispose();
|
||||
}
|
||||
/// <inheritdoc/>
|
||||
public ChannelReceivedEventHandler ChannelReceived { get; } = new();
|
||||
@@ -65,22 +73,25 @@ public class OtherChannel : SetupConfigObject, IClientChannel
|
||||
/// <summary>
|
||||
/// 等待池
|
||||
/// </summary>
|
||||
public WaitHandlePool<MessageBase> WaitHandlePool { get; internal set; } = new();
|
||||
public WaitHandlePool<MessageBase> WaitHandlePool { get; internal set; } = new(0, ushort.MaxValue);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public WaitLock WaitLock => ChannelOptions.WaitLock;
|
||||
public virtual WaitLock GetLock(string key) => WaitLock;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ConcurrentDictionary<long, Func<IClientChannel, ReceivedDataEventArgs, bool, Task>> ChannelReceivedWaitDict { get; } = new();
|
||||
|
||||
|
||||
//private readonly WaitLock _connectLock = new WaitLock();
|
||||
|
||||
private IDeviceDataHandleAdapter _deviceDataHandleAdapter;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void SetDataHandlingAdapter(DataHandlingAdapter adapter)
|
||||
{
|
||||
if (adapter is SingleStreamDataHandlingAdapter singleStreamDataHandlingAdapter)
|
||||
SetAdapter(singleStreamDataHandlingAdapter);
|
||||
if (adapter is IDeviceDataHandleAdapter deviceDataHandleAdapter)
|
||||
_deviceDataHandleAdapter = deviceDataHandleAdapter;
|
||||
}
|
||||
/// <summary>
|
||||
/// 设置数据处理适配器。
|
||||
@@ -104,20 +115,17 @@ public class OtherChannel : SetupConfigObject, IClientChannel
|
||||
}
|
||||
|
||||
// 设置适配器的日志记录器和加载、接收数据的回调方法。
|
||||
adapter.Logger = Logger;
|
||||
adapter.OnLoaded(this);
|
||||
adapter.ReceivedAsyncCallBack = PrivateHandleReceivedData;
|
||||
//adapter.SendCallBack = this.ProtectedDefaultSend;
|
||||
adapter.SendAsyncCallBack = ProtectedDefaultSendAsync;
|
||||
|
||||
// 将提供的适配器实例设置为当前实例的数据处理适配器。
|
||||
m_dataHandlingAdapter = adapter;
|
||||
}
|
||||
|
||||
private async Task PrivateHandleReceivedData(IByteBlockReader byteBlock, IRequestInfo requestInfo)
|
||||
private Task PrivateHandleReceivedData(ReadOnlyMemory<byte> byteBlock, IRequestInfo requestInfo)
|
||||
{
|
||||
LastReceivedTime = DateTime.Now;
|
||||
await this.OnChannelReceivedEvent(new ReceivedDataEventArgs(byteBlock, requestInfo), ChannelReceived).ConfigureAwait(false);
|
||||
return this.OnChannelReceivedEvent(new ReceivedDataEventArgs(byteBlock, requestInfo), ChannelReceived);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -154,7 +162,8 @@ public class OtherChannel : SetupConfigObject, IClientChannel
|
||||
return Task.FromResult(Result.Success);
|
||||
}
|
||||
public volatile bool online;
|
||||
public Task ConnectAsync(int millisecondsTimeout, CancellationToken token)
|
||||
|
||||
public Task ConnectAsync(CancellationToken token)
|
||||
{
|
||||
var cts = m_transport;
|
||||
m_transport = new();
|
||||
@@ -180,8 +189,11 @@ public class OtherChannel : SetupConfigObject, IClientChannel
|
||||
}
|
||||
else
|
||||
{
|
||||
// 否则,使用适配器的发送方法进行数据发送。
|
||||
return m_dataHandlingAdapter.SendInputAsync(memory, cancellationToken);
|
||||
var byteBlock = new ByteBlock(1024);
|
||||
m_dataHandlingAdapter.SendInput(ref byteBlock, memory);
|
||||
|
||||
byteBlock.SafeDispose();
|
||||
return EasyTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,9 +202,14 @@ public class OtherChannel : SetupConfigObject, IClientChannel
|
||||
// 检查是否具备发送请求的条件,如果不具备则抛出异常
|
||||
ThrowIfCannotSendRequestInfo();
|
||||
|
||||
// 使用数据处理适配器异步发送输入请求
|
||||
return m_dataHandlingAdapter.SendInputAsync(requestInfo, cancellationToken);
|
||||
var byteBlock = new ByteBlock(1024);
|
||||
m_dataHandlingAdapter.SendInput(ref byteBlock, requestInfo);
|
||||
|
||||
byteBlock.SafeDispose();
|
||||
return EasyTask.CompletedTask;
|
||||
}
|
||||
|
||||
|
||||
private void ThrowIfCannotSendRequestInfo()
|
||||
{
|
||||
if (m_dataHandlingAdapter?.CanSendRequestInfo != true)
|
||||
@@ -200,4 +217,6 @@ public class OtherChannel : SetupConfigObject, IClientChannel
|
||||
throw new NotSupportedException($"当前适配器为空或者不支持对象发送。");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@@ -59,19 +59,18 @@ public class DtuPlugin : PluginBase, ITcpReceivingPlugin
|
||||
public bool DtuIdHex { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task OnTcpReceiving(ITcpSession client, ByteBlockEventArgs e)
|
||||
public async Task OnTcpReceiving(ITcpSession client, BytesReaderEventArgs e)
|
||||
{
|
||||
var len = HeartbeatByte.Length;
|
||||
if (client is TcpSessionClientChannel socket && socket.Service is ITcpServiceChannel tcpServiceChannel)
|
||||
{
|
||||
if (!socket.Id.StartsWith("ID="))
|
||||
{
|
||||
var id = DtuIdHex ? $"ID={e.ByteBlock.Span.ToHexString()}" : $"ID={e.ByteBlock.ToString(0, e.ByteBlock.Length)}";
|
||||
var id = DtuIdHex ? $"ID={e.Reader.ToHexString()}" : $"ID={e.Reader.TotalSequence.ToString(Encoding.UTF8)}";
|
||||
if (tcpServiceChannel.TryGetClient(id, out var oldClient))
|
||||
{
|
||||
try
|
||||
{
|
||||
//await oldClient.ShutdownAsync(System.Net.Sockets.SocketShutdown.Both).ConfigureAwait(false);
|
||||
await oldClient.CloseAsync().ConfigureAwait(false);
|
||||
oldClient.Dispose();
|
||||
}
|
||||
@@ -79,8 +78,9 @@ public class DtuPlugin : PluginBase, ITcpReceivingPlugin
|
||||
{
|
||||
}
|
||||
}
|
||||
await socket.ResetIdAsync(id).ConfigureAwait(false);
|
||||
await socket.ResetIdAsync(id, client.ClosedToken).ConfigureAwait(false);
|
||||
client.Logger?.Info(string.Format(AppResource.DtuConnected, id));
|
||||
e.Reader.Advance((int)e.Reader.BytesRemaining);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
@@ -88,7 +88,6 @@ public class DtuPlugin : PluginBase, ITcpReceivingPlugin
|
||||
{
|
||||
try
|
||||
{
|
||||
//await socket.ShutdownAsync(System.Net.Sockets.SocketShutdown.Both).ConfigureAwait(false);
|
||||
await socket.CloseAsync().ConfigureAwait(false);
|
||||
socket.Dispose();
|
||||
}
|
||||
@@ -102,14 +101,15 @@ public class DtuPlugin : PluginBase, ITcpReceivingPlugin
|
||||
|
||||
if (len > 0)
|
||||
{
|
||||
if (HeartbeatByte.Span.SequenceEqual(e.ByteBlock.Memory.Slice(0, len).Span))
|
||||
if (HeartbeatByte.Span.SequenceEqual(e.Reader.TotalSequence.Slice(0, (int)Math.Min(len, e.Reader.BytesRemaining + e.Reader.BytesRead)).First.Span))
|
||||
{
|
||||
if (DateTimeOffset.Now - socket.LastSentTime < TimeSpan.FromMilliseconds(200))
|
||||
{
|
||||
await Task.Delay(200).ConfigureAwait(false);
|
||||
await Task.Delay(200, client.ClosedToken).ConfigureAwait(false);
|
||||
}
|
||||
//回应心跳包
|
||||
await socket.SendAsync(HeartbeatByte, socket.ClosedToken).ConfigureAwait(false);
|
||||
e.Reader.Advance((int)Math.Min(len, e.Reader.BytesRemaining));
|
||||
e.Handled = true;
|
||||
if (socket.Logger?.LogLevel <= LogLevel.Trace)
|
||||
socket.Logger?.Trace($"{socket}- Heartbeat");
|
||||
@@ -118,4 +118,6 @@ public class DtuPlugin : PluginBase, ITcpReceivingPlugin
|
||||
}
|
||||
await e.InvokeNext().ConfigureAwait(false);//如果本插件无法处理当前数据,请将数据转至下一个插件。
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@@ -105,7 +105,7 @@ internal sealed class HeartbeatAndReceivePlugin : PluginBase, ITcpConnectedPlugi
|
||||
}
|
||||
}
|
||||
}
|
||||
private Task Task;
|
||||
private Task _task;
|
||||
private bool SendHeartbeat;
|
||||
public int HeartbeatTime { get; set; } = 3000;
|
||||
|
||||
@@ -125,17 +125,17 @@ internal sealed class HeartbeatAndReceivePlugin : PluginBase, ITcpConnectedPlugi
|
||||
{
|
||||
await tcpClient.SendAsync(DtuIdByte, tcpClient.ClosedToken).ConfigureAwait(false);
|
||||
|
||||
if (Task == null)
|
||||
if (_task == null)
|
||||
{
|
||||
Task = Task.Factory.StartNew(async () =>
|
||||
_task = Task.Run(async () =>
|
||||
{
|
||||
var failedCount = 0;
|
||||
while (SendHeartbeat)
|
||||
{
|
||||
await Task.Delay(HeartbeatTime).ConfigureAwait(false);
|
||||
await Task.Delay(HeartbeatTime, client.ClosedToken).ConfigureAwait(false);
|
||||
if (!client.Online)
|
||||
{
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
|
||||
try
|
||||
@@ -159,15 +159,15 @@ internal sealed class HeartbeatAndReceivePlugin : PluginBase, ITcpConnectedPlugi
|
||||
}
|
||||
}
|
||||
|
||||
Task = null;
|
||||
}, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default);
|
||||
_task = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await e.InvokeNext().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async Task OnTcpReceiving(ITcpSession client, ByteBlockEventArgs e)
|
||||
public async Task OnTcpReceiving(ITcpSession client, BytesReaderEventArgs e)
|
||||
{
|
||||
if (client is ITcpSessionClient)
|
||||
{
|
||||
@@ -181,8 +181,9 @@ internal sealed class HeartbeatAndReceivePlugin : PluginBase, ITcpConnectedPlugi
|
||||
var len = HeartbeatByte.Length;
|
||||
if (len > 0)
|
||||
{
|
||||
if (HeartbeatByte.Span.SequenceEqual(e.ByteBlock.Memory.Slice(0, len).Span))
|
||||
if (HeartbeatByte.Span.SequenceEqual(e.Reader.TotalSequence.Slice(0, (int)Math.Min(len, e.Reader.BytesRemaining + e.Reader.BytesRead)).First.Span))
|
||||
{
|
||||
e.Reader.Advance((int)Math.Min(len, e.Reader.BytesRemaining));
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
@@ -190,6 +191,7 @@ internal sealed class HeartbeatAndReceivePlugin : PluginBase, ITcpConnectedPlugi
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public Task OnTcpClosed(ITcpSession client, ClosedEventArgs e)
|
||||
{
|
||||
SendHeartbeat = false;
|
||||
|
@@ -77,10 +77,9 @@ public static class PluginUtil
|
||||
a.UseTcpSessionCheckClear()
|
||||
.SetCheckClearType(CheckClearType.All)
|
||||
.SetTick(TimeSpan.FromMilliseconds(channelOptions.CheckClearTime))
|
||||
.SetOnClose(async (c, t) =>
|
||||
.SetOnClose((c, t) =>
|
||||
{
|
||||
//await c.ShutdownAsync(System.Net.Sockets.SocketShutdown.Both).ConfigureAwait(false);
|
||||
await c.CloseAsync($"{channelOptions.CheckClearTime}ms Timeout").ConfigureAwait(false);
|
||||
return c.CloseAsync($"{channelOptions.CheckClearTime}ms Timeout");
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@@ -8,8 +8,6 @@
|
||||
// QQ群:605534569
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using ThingsGateway.NewLife;
|
||||
|
||||
using TouchSocket.SerialPorts;
|
||||
@@ -34,7 +32,6 @@ public class SerialPortChannel : SerialPortClient, IClientChannel
|
||||
var pool = WaitHandlePool;
|
||||
WaitHandlePool = new WaitHandlePool<MessageBase>(minSign, maxSign);
|
||||
pool?.CancelAll();
|
||||
pool?.SafeDispose();
|
||||
}
|
||||
/// <inheritdoc/>
|
||||
public ChannelReceivedEventHandler ChannelReceived { get; } = new();
|
||||
@@ -50,6 +47,26 @@ public class SerialPortChannel : SerialPortClient, IClientChannel
|
||||
|
||||
/// <inheritdoc/>
|
||||
public DataHandlingAdapter ReadOnlyDataHandlingAdapter => ProtectedDataHandlingAdapter;
|
||||
private IDeviceDataHandleAdapter _deviceDataHandleAdapter;
|
||||
public void SetDataHandlingAdapterLogger(ILog log)
|
||||
{
|
||||
if (_deviceDataHandleAdapter != ProtectedDataHandlingAdapter && ProtectedDataHandlingAdapter is IDeviceDataHandleAdapter handleAdapter)
|
||||
{
|
||||
_deviceDataHandleAdapter = handleAdapter;
|
||||
}
|
||||
if (_deviceDataHandleAdapter != null)
|
||||
{
|
||||
_deviceDataHandleAdapter.Logger = log;
|
||||
}
|
||||
}
|
||||
/// <inheritdoc/>
|
||||
public void SetDataHandlingAdapter(DataHandlingAdapter adapter)
|
||||
{
|
||||
if (adapter is SingleStreamDataHandlingAdapter singleStreamDataHandlingAdapter)
|
||||
SetAdapter(singleStreamDataHandlingAdapter);
|
||||
if (adapter is IDeviceDataHandleAdapter deviceDataHandleAdapter)
|
||||
_deviceDataHandleAdapter = deviceDataHandleAdapter;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ChannelEventHandler Started { get; } = new();
|
||||
@@ -65,14 +82,13 @@ public class SerialPortChannel : SerialPortClient, IClientChannel
|
||||
/// <summary>
|
||||
/// 等待池
|
||||
/// </summary>
|
||||
public WaitHandlePool<MessageBase> WaitHandlePool { get; internal set; } = new();
|
||||
public WaitHandlePool<MessageBase> WaitHandlePool { get; internal set; } = new(0, ushort.MaxValue);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public WaitLock WaitLock => ChannelOptions.WaitLock;
|
||||
public virtual WaitLock GetLock(string key) => WaitLock;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ConcurrentDictionary<long, Func<IClientChannel, ReceivedDataEventArgs, bool, Task>> ChannelReceivedWaitDict { get; } = new();
|
||||
|
||||
|
||||
//private readonly WaitLock _connectLock = new WaitLock();
|
||||
/// <inheritdoc/>
|
||||
@@ -86,6 +102,7 @@ public class SerialPortChannel : SerialPortClient, IClientChannel
|
||||
if (Online)
|
||||
{
|
||||
PortName = null;
|
||||
await this.OnChannelEvent(Stoping).ConfigureAwait(false);
|
||||
var result = await base.CloseAsync(msg, token).ConfigureAwait(false);
|
||||
if (!Online)
|
||||
{
|
||||
@@ -103,7 +120,7 @@ public class SerialPortChannel : SerialPortClient, IClientChannel
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task ConnectAsync(int millisecondsTimeout, CancellationToken token)
|
||||
public override async Task ConnectAsync(CancellationToken token)
|
||||
{
|
||||
if (!Online)
|
||||
{
|
||||
@@ -119,7 +136,8 @@ public class SerialPortChannel : SerialPortClient, IClientChannel
|
||||
if (port != null)
|
||||
PortName = $"{port.PortName}";
|
||||
|
||||
await base.ConnectAsync(millisecondsTimeout, token).ConfigureAwait(false);
|
||||
await this.OnChannelEvent(Starting).ConfigureAwait(false);
|
||||
await base.ConnectAsync(token).ConfigureAwait(false);
|
||||
if (Online)
|
||||
{
|
||||
if (token.IsCancellationRequested) return;
|
||||
@@ -134,12 +152,7 @@ public class SerialPortChannel : SerialPortClient, IClientChannel
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void SetDataHandlingAdapter(DataHandlingAdapter adapter)
|
||||
{
|
||||
if (adapter is SingleStreamDataHandlingAdapter singleStreamDataHandlingAdapter)
|
||||
SetAdapter(singleStreamDataHandlingAdapter);
|
||||
}
|
||||
|
||||
private string PortName { get; set; }
|
||||
/// <inheritdoc/>
|
||||
public override string? ToString()
|
||||
@@ -153,54 +166,48 @@ public class SerialPortChannel : SerialPortClient, IClientChannel
|
||||
return base.ToString();
|
||||
}
|
||||
|
||||
protected override async Task OnSerialClosed(ClosedEventArgs e)
|
||||
protected override Task OnSerialClosed(ClosedEventArgs e)
|
||||
{
|
||||
Logger?.Info($"{ToString()} Closed{(e.Message.IsNullOrEmpty() ? string.Empty : $" -{e.Message}")}");
|
||||
|
||||
await base.OnSerialClosed(e).ConfigureAwait(false);
|
||||
return base.OnSerialClosed(e);
|
||||
}
|
||||
/// <inheritdoc/>
|
||||
protected override async Task OnSerialClosing(ClosingEventArgs e)
|
||||
protected override Task OnSerialClosing(ClosingEventArgs e)
|
||||
{
|
||||
await this.OnChannelEvent(Stoping).ConfigureAwait(false);
|
||||
Logger?.Trace($"{ToString()} Closing{(e.Message.IsNullOrEmpty() ? string.Empty : $" -{e.Message}")}");
|
||||
await base.OnSerialClosing(e).ConfigureAwait(false);
|
||||
return base.OnSerialClosing(e);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async Task OnSerialConnecting(ConnectingEventArgs e)
|
||||
protected override Task OnSerialConnecting(ConnectingEventArgs e)
|
||||
{
|
||||
Logger?.Trace($"{ToString()} Connecting{(e.Message.IsNullOrEmpty() ? string.Empty : $" -{e.Message}")}");
|
||||
await this.OnChannelEvent(Starting).ConfigureAwait(false);
|
||||
await base.OnSerialConnecting(e).ConfigureAwait(false);
|
||||
return base.OnSerialConnecting(e);
|
||||
}
|
||||
protected override async Task OnSerialConnected(ConnectedEventArgs e)
|
||||
protected override Task OnSerialConnected(ConnectedEventArgs e)
|
||||
{
|
||||
Logger?.Debug($"{ToString()} Connected");
|
||||
await base.OnSerialConnected(e).ConfigureAwait(false);
|
||||
return base.OnSerialConnected(e);
|
||||
}
|
||||
/// <inheritdoc/>
|
||||
protected override async Task OnSerialReceived(ReceivedDataEventArgs e)
|
||||
{
|
||||
await base.OnSerialReceived(e).ConfigureAwait(false);
|
||||
if (e.RequestInfo is MessageBase response)
|
||||
{
|
||||
if (ChannelReceivedWaitDict.TryRemove(response.Sign, out var func))
|
||||
{
|
||||
await func.Invoke(this, e, ChannelReceived.Count == 1).ConfigureAwait(false);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
var receivedTask = base.OnSerialReceived(e);
|
||||
if (!receivedTask.IsCompleted)
|
||||
await receivedTask.ConfigureAwait(false);
|
||||
|
||||
if (e.Handled)
|
||||
return;
|
||||
|
||||
await this.OnChannelReceivedEvent(e, ChannelReceived).ConfigureAwait(false);
|
||||
}
|
||||
var channelReceivedTask = this.OnChannelReceivedEvent(e, ChannelReceived);
|
||||
if (!channelReceivedTask.IsCompleted)
|
||||
await channelReceivedTask.ConfigureAwait(false);
|
||||
|
||||
}
|
||||
/// <inheritdoc/>
|
||||
protected override void SafetyDispose(bool disposing)
|
||||
{
|
||||
WaitHandlePool.SafeDispose();
|
||||
WaitHandlePool?.CancelAll();
|
||||
base.SafetyDispose(disposing);
|
||||
}
|
||||
}
|
||||
|
@@ -8,8 +8,6 @@
|
||||
// QQ群:605534569
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using ThingsGateway.NewLife;
|
||||
|
||||
namespace ThingsGateway.Foundation;
|
||||
@@ -31,9 +29,27 @@ public class TcpClientChannel : TcpClient, IClientChannel
|
||||
var pool = WaitHandlePool;
|
||||
WaitHandlePool = new WaitHandlePool<MessageBase>(minSign, maxSign);
|
||||
pool?.CancelAll();
|
||||
pool?.SafeDispose();
|
||||
}
|
||||
|
||||
private IDeviceDataHandleAdapter _deviceDataHandleAdapter;
|
||||
public void SetDataHandlingAdapterLogger(ILog log)
|
||||
{
|
||||
if (_deviceDataHandleAdapter != DataHandlingAdapter && DataHandlingAdapter is IDeviceDataHandleAdapter handleAdapter)
|
||||
{
|
||||
_deviceDataHandleAdapter = handleAdapter;
|
||||
}
|
||||
if (_deviceDataHandleAdapter != null)
|
||||
{
|
||||
_deviceDataHandleAdapter.Logger = log;
|
||||
}
|
||||
}
|
||||
/// <inheritdoc/>
|
||||
public void SetDataHandlingAdapter(DataHandlingAdapter adapter)
|
||||
{
|
||||
if (adapter is SingleStreamDataHandlingAdapter singleStreamDataHandlingAdapter)
|
||||
SetAdapter(singleStreamDataHandlingAdapter);
|
||||
if (adapter is IDeviceDataHandleAdapter deviceDataHandleAdapter)
|
||||
_deviceDataHandleAdapter = deviceDataHandleAdapter;
|
||||
}
|
||||
/// <inheritdoc/>
|
||||
public ChannelReceivedEventHandler ChannelReceived { get; } = new();
|
||||
|
||||
@@ -62,14 +78,13 @@ public class TcpClientChannel : TcpClient, IClientChannel
|
||||
/// <summary>
|
||||
/// 等待池
|
||||
/// </summary>
|
||||
public WaitHandlePool<MessageBase> WaitHandlePool { get; internal set; } = new();
|
||||
public WaitHandlePool<MessageBase> WaitHandlePool { get; internal set; } = new(0, ushort.MaxValue);
|
||||
public virtual WaitLock GetLock(string key) => WaitLock;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public WaitLock WaitLock => ChannelOptions.WaitLock;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ConcurrentDictionary<long, Func<IClientChannel, ReceivedDataEventArgs, bool, Task>> ChannelReceivedWaitDict { get; } = new();
|
||||
|
||||
|
||||
//private readonly WaitLock _connectLock = new WaitLock();
|
||||
/// <inheritdoc/>
|
||||
@@ -82,6 +97,7 @@ public class TcpClientChannel : TcpClient, IClientChannel
|
||||
//await _connectLock.WaitAsync().ConfigureAwait(false);
|
||||
if (Online)
|
||||
{
|
||||
await this.OnChannelEvent(Stoping).ConfigureAwait(false);
|
||||
var result = await base.CloseAsync(msg, token).ConfigureAwait(false);
|
||||
if (!Online)
|
||||
{
|
||||
@@ -99,7 +115,7 @@ public class TcpClientChannel : TcpClient, IClientChannel
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task ConnectAsync(int millisecondsTimeout, CancellationToken token)
|
||||
public override async Task ConnectAsync(CancellationToken token)
|
||||
{
|
||||
if (!Online)
|
||||
{
|
||||
@@ -109,7 +125,8 @@ public class TcpClientChannel : TcpClient, IClientChannel
|
||||
if (!Online)
|
||||
{
|
||||
if (token.IsCancellationRequested) return;
|
||||
await base.ConnectAsync(millisecondsTimeout, token).ConfigureAwait(false);
|
||||
await this.OnChannelEvent(Starting).ConfigureAwait(false);
|
||||
await base.ConnectAsync(token).ConfigureAwait(false);
|
||||
if (Online)
|
||||
{
|
||||
if (token.IsCancellationRequested) return;
|
||||
@@ -124,12 +141,6 @@ public class TcpClientChannel : TcpClient, IClientChannel
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void SetDataHandlingAdapter(DataHandlingAdapter adapter)
|
||||
{
|
||||
if (adapter is SingleStreamDataHandlingAdapter singleStreamDataHandlingAdapter)
|
||||
SetAdapter(singleStreamDataHandlingAdapter);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
@@ -137,58 +148,53 @@ public class TcpClientChannel : TcpClient, IClientChannel
|
||||
return $"{IP}:{Port}";
|
||||
}
|
||||
|
||||
protected override async Task OnTcpClosed(ClosedEventArgs e)
|
||||
protected override Task OnTcpClosed(ClosedEventArgs e)
|
||||
{
|
||||
Logger?.Info($"{ToString()} Closed{(e.Message.IsNullOrEmpty() ? string.Empty : $" -{e.Message}")}");
|
||||
|
||||
await base.OnTcpClosed(e).ConfigureAwait(false);
|
||||
return base.OnTcpClosed(e);
|
||||
}
|
||||
/// <inheritdoc/>
|
||||
protected override async Task OnTcpClosing(ClosingEventArgs e)
|
||||
protected override Task OnTcpClosing(ClosingEventArgs e)
|
||||
{
|
||||
await this.OnChannelEvent(Stoping).ConfigureAwait(false);
|
||||
Logger?.Trace($"{ToString()} Closing{(e.Message.IsNullOrEmpty() ? string.Empty : $" -{e.Message}")}");
|
||||
|
||||
await base.OnTcpClosing(e).ConfigureAwait(false);
|
||||
return base.OnTcpClosing(e);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async Task OnTcpConnecting(ConnectingEventArgs e)
|
||||
protected override Task OnTcpConnecting(ConnectingEventArgs e)
|
||||
{
|
||||
Logger?.Trace($"{ToString()} Connecting{(e.Message.IsNullOrEmpty() ? string.Empty : $"-{e.Message}")}");
|
||||
await this.OnChannelEvent(Starting).ConfigureAwait(false);
|
||||
await base.OnTcpConnecting(e).ConfigureAwait(false);
|
||||
return base.OnTcpConnecting(e);
|
||||
}
|
||||
|
||||
protected override async Task OnTcpConnected(ConnectedEventArgs e)
|
||||
protected override Task OnTcpConnected(ConnectedEventArgs e)
|
||||
{
|
||||
Logger?.Info($"{ToString()} Connected");
|
||||
|
||||
await base.OnTcpConnected(e).ConfigureAwait(false);
|
||||
return base.OnTcpConnected(e);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async Task OnTcpReceived(ReceivedDataEventArgs e)
|
||||
{
|
||||
await base.OnTcpReceived(e).ConfigureAwait(false);
|
||||
if (e.RequestInfo is MessageBase response)
|
||||
{
|
||||
if (ChannelReceivedWaitDict.TryRemove(response.Sign, out var func))
|
||||
{
|
||||
await func.Invoke(this, e, ChannelReceived.Count == 1).ConfigureAwait(false);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
var receivedTask = base.OnTcpReceived(e);
|
||||
if (!receivedTask.IsCompleted)
|
||||
await receivedTask.ConfigureAwait(false);
|
||||
|
||||
if (e.Handled)
|
||||
return;
|
||||
|
||||
await this.OnChannelReceivedEvent(e, ChannelReceived).ConfigureAwait(false);
|
||||
var channelReceivedTask = this.OnChannelReceivedEvent(e, ChannelReceived);
|
||||
if (!channelReceivedTask.IsCompleted)
|
||||
await channelReceivedTask.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void SafetyDispose(bool disposing)
|
||||
{
|
||||
WaitHandlePool.SafeDispose();
|
||||
WaitHandlePool?.CancelAll();
|
||||
base.SafetyDispose(disposing);
|
||||
}
|
||||
}
|
||||
|
@@ -8,8 +8,6 @@
|
||||
// QQ群:605534569
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using ThingsGateway.NewLife;
|
||||
|
||||
namespace ThingsGateway.Foundation;
|
||||
@@ -23,30 +21,6 @@ public abstract class TcpServiceChannelBase<TClient> : TcpService<TClient>, ITcp
|
||||
/// <inheritdoc/>
|
||||
public ConcurrentList<IDevice> Collects { get; } = new();
|
||||
|
||||
///// <summary>
|
||||
///// 停止时是否发送ShutDown
|
||||
///// </summary>
|
||||
//public bool ShutDownEnable { get; set; } = true;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override async Task ClearAsync()
|
||||
{
|
||||
foreach (var client in Clients)
|
||||
{
|
||||
try
|
||||
{
|
||||
//if (ShutDownEnable)
|
||||
// await client.ShutdownAsync(System.Net.Sockets.SocketShutdown.Both).ConfigureAwait(false);
|
||||
|
||||
await client.CloseAsync().ConfigureAwait(false);
|
||||
client.SafeDispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ClientDisposeAsync(string id)
|
||||
{
|
||||
if (this.TryGetClient(id, out var client))
|
||||
@@ -136,6 +110,7 @@ public abstract class TcpServiceChannelBase<TClient> : TcpService<TClient>, ITcp
|
||||
{
|
||||
m_transport?.SafeCancel();
|
||||
m_transport?.SafeDispose();
|
||||
m_transport = null;
|
||||
base.SafetyDispose(disposing);
|
||||
}
|
||||
/// <inheritdoc/>
|
||||
@@ -209,7 +184,7 @@ public class TcpServiceChannel<TClient> : TcpServiceChannelBase<TClient>, IChann
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task ConnectAsync(int timeout = 3000, CancellationToken token = default)
|
||||
public Task ConnectAsync(CancellationToken token = default)
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
return EasyTask.CompletedTask;
|
||||
@@ -228,8 +203,8 @@ public class TcpServiceChannel<TClient> : TcpServiceChannelBase<TClient>, IChann
|
||||
data.ResetSign(MinSign, MaxSign);
|
||||
return data;
|
||||
}
|
||||
public int MaxSign { get; private set; } = 0;
|
||||
public int MinSign { get; private set; } = ushort.MaxValue;
|
||||
public int MinSign { get; private set; } = 0;
|
||||
public int MaxSign { get; private set; } = ushort.MaxValue;
|
||||
public void ResetSign(int minSign = 0, int maxSign = ushort.MaxValue)
|
||||
{
|
||||
MinSign = minSign;
|
||||
@@ -266,24 +241,20 @@ public class TcpServiceChannel<TClient> : TcpServiceChannelBase<TClient>, IChann
|
||||
/// <inheritdoc/>
|
||||
protected override async Task OnTcpReceived(TClient socketClient, ReceivedDataEventArgs e)
|
||||
{
|
||||
await base.OnTcpReceived(socketClient, e).ConfigureAwait(false);
|
||||
var receivedTask = base.OnTcpReceived(socketClient, e);
|
||||
if (!receivedTask.IsCompleted)
|
||||
await receivedTask.ConfigureAwait(false);
|
||||
|
||||
if (e.RequestInfo is MessageBase response)
|
||||
{
|
||||
if (ChannelReceivedWaitDict.TryRemove(response.Sign, out var func))
|
||||
{
|
||||
await func.Invoke(socketClient, e, ChannelReceived.Count == 1).ConfigureAwait(false);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
if (e.Handled)
|
||||
return;
|
||||
|
||||
await socketClient.OnChannelReceivedEvent(e, ChannelReceived).ConfigureAwait(false);
|
||||
var channelReceivedTask = socketClient.OnChannelReceivedEvent(e, ChannelReceived);
|
||||
if (!channelReceivedTask.IsCompleted)
|
||||
await channelReceivedTask.ConfigureAwait(false);
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ConcurrentDictionary<long, Func<IClientChannel, ReceivedDataEventArgs, bool, Task>> ChannelReceivedWaitDict { get; } = new();
|
||||
|
||||
|
||||
IEnumerable<TcpSessionClientChannel> ITcpServiceChannel.Clients => base.Clients;
|
||||
|
||||
|
@@ -8,8 +8,6 @@
|
||||
// QQ群:605534569
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using ThingsGateway.NewLife;
|
||||
|
||||
namespace ThingsGateway.Foundation;
|
||||
@@ -23,13 +21,31 @@ public class TcpSessionClientChannel : TcpSessionClient, IClientChannel
|
||||
public TcpSessionClientChannel()
|
||||
{
|
||||
}
|
||||
|
||||
private IDeviceDataHandleAdapter _deviceDataHandleAdapter;
|
||||
public void SetDataHandlingAdapterLogger(ILog log)
|
||||
{
|
||||
if (_deviceDataHandleAdapter != DataHandlingAdapter && DataHandlingAdapter is IDeviceDataHandleAdapter handleAdapter)
|
||||
{
|
||||
_deviceDataHandleAdapter = handleAdapter;
|
||||
}
|
||||
if (_deviceDataHandleAdapter != null)
|
||||
{
|
||||
_deviceDataHandleAdapter.Logger = log;
|
||||
}
|
||||
}
|
||||
/// <inheritdoc/>
|
||||
public void SetDataHandlingAdapter(DataHandlingAdapter adapter)
|
||||
{
|
||||
if (adapter is SingleStreamDataHandlingAdapter singleStreamDataHandlingAdapter)
|
||||
SetAdapter(singleStreamDataHandlingAdapter);
|
||||
if (adapter is IDeviceDataHandleAdapter deviceDataHandleAdapter)
|
||||
_deviceDataHandleAdapter = deviceDataHandleAdapter;
|
||||
}
|
||||
public void ResetSign(int minSign = 0, int maxSign = ushort.MaxValue)
|
||||
{
|
||||
var pool = WaitHandlePool;
|
||||
WaitHandlePool = new WaitHandlePool<MessageBase>(minSign, maxSign);
|
||||
pool?.CancelAll();
|
||||
pool?.SafeDispose();
|
||||
}
|
||||
/// <inheritdoc/>
|
||||
public ChannelReceivedEventHandler ChannelReceived { get; } = new();
|
||||
@@ -60,7 +76,7 @@ public class TcpSessionClientChannel : TcpSessionClient, IClientChannel
|
||||
/// <summary>
|
||||
/// 等待池
|
||||
/// </summary>
|
||||
public WaitHandlePool<MessageBase> WaitHandlePool { get; private set; } = new();
|
||||
public WaitHandlePool<MessageBase> WaitHandlePool { get; private set; } = new(0, ushort.MaxValue);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public WaitLock WaitLock { get; internal set; } = new(nameof(TcpSessionClientChannel));
|
||||
@@ -69,25 +85,19 @@ public class TcpSessionClientChannel : TcpSessionClient, IClientChannel
|
||||
/// <inheritdoc/>
|
||||
public override Task<Result> CloseAsync(string msg, CancellationToken token)
|
||||
{
|
||||
WaitHandlePool.SafeDispose();
|
||||
WaitHandlePool?.CancelAll();
|
||||
return base.CloseAsync(msg, token);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task ConnectAsync(int timeout, CancellationToken token) => Task.CompletedTask;
|
||||
public Task ConnectAsync(CancellationToken token) => Task.CompletedTask;
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void SetDataHandlingAdapter(DataHandlingAdapter adapter)
|
||||
{
|
||||
if (adapter is SingleStreamDataHandlingAdapter singleStreamDataHandlingAdapter)
|
||||
SetAdapter(singleStreamDataHandlingAdapter);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task SetupAsync(TouchSocketConfig config) => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ConcurrentDictionary<long, Func<IClientChannel, ReceivedDataEventArgs, bool, Task>> ChannelReceivedWaitDict { get; } = new();
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
@@ -98,7 +108,7 @@ public class TcpSessionClientChannel : TcpSessionClient, IClientChannel
|
||||
/// <inheritdoc/>
|
||||
protected override void SafetyDispose(bool disposing)
|
||||
{
|
||||
WaitHandlePool.SafeDispose();
|
||||
WaitHandlePool?.CancelAll();
|
||||
base.SafetyDispose(disposing);
|
||||
}
|
||||
|
||||
@@ -135,18 +145,15 @@ public class TcpSessionClientChannel : TcpSessionClient, IClientChannel
|
||||
/// <inheritdoc/>
|
||||
protected override async Task OnTcpReceived(ReceivedDataEventArgs e)
|
||||
{
|
||||
await base.OnTcpReceived(e).ConfigureAwait(false);
|
||||
if (e.RequestInfo is MessageBase response)
|
||||
{
|
||||
if (ChannelReceivedWaitDict.TryRemove(response.Sign, out var func))
|
||||
{
|
||||
await func.Invoke(this, e, ChannelReceived.Count == 1).ConfigureAwait(false);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
var receivedTask = base.OnTcpReceived(e);
|
||||
if (!receivedTask.IsCompleted)
|
||||
await receivedTask.ConfigureAwait(false);
|
||||
|
||||
if (e.Handled)
|
||||
return;
|
||||
|
||||
await this.OnChannelReceivedEvent(e, ChannelReceived).ConfigureAwait(false);
|
||||
var channelReceivedTask = this.OnChannelReceivedEvent(e, ChannelReceived);
|
||||
if (!channelReceivedTask.IsCompleted)
|
||||
await channelReceivedTask.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
@@ -8,8 +8,6 @@
|
||||
// QQ群:605534569
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using ThingsGateway.NewLife;
|
||||
|
||||
namespace ThingsGateway.Foundation;
|
||||
@@ -28,13 +26,31 @@ public class UdpSessionChannel : UdpSession, IClientChannel
|
||||
ResetSign();
|
||||
}
|
||||
public override TouchSocketConfig Config => base.Config ?? ChannelOptions.Config;
|
||||
|
||||
private IDeviceDataHandleAdapter _deviceDataHandleAdapter;
|
||||
public void SetDataHandlingAdapterLogger(ILog log)
|
||||
{
|
||||
if (_deviceDataHandleAdapter != DataHandlingAdapter && DataHandlingAdapter is IDeviceDataHandleAdapter handleAdapter)
|
||||
{
|
||||
_deviceDataHandleAdapter = handleAdapter;
|
||||
}
|
||||
if (_deviceDataHandleAdapter != null)
|
||||
{
|
||||
_deviceDataHandleAdapter.Logger = log;
|
||||
}
|
||||
}
|
||||
/// <inheritdoc/>
|
||||
public void SetDataHandlingAdapter(DataHandlingAdapter adapter)
|
||||
{
|
||||
if (adapter is UdpDataHandlingAdapter udpDataHandlingAdapter)
|
||||
SetAdapter(udpDataHandlingAdapter);
|
||||
if (adapter is IDeviceDataHandleAdapter deviceDataHandleAdapter)
|
||||
_deviceDataHandleAdapter = deviceDataHandleAdapter;
|
||||
}
|
||||
public void ResetSign(int minSign = 0, int maxSign = ushort.MaxValue)
|
||||
{
|
||||
var pool = WaitHandlePool;
|
||||
WaitHandlePool = new WaitHandlePool<MessageBase>(minSign, maxSign);
|
||||
pool?.CancelAll();
|
||||
pool?.SafeDispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -69,14 +85,13 @@ public class UdpSessionChannel : UdpSession, IClientChannel
|
||||
/// <summary>
|
||||
/// 等待池
|
||||
/// </summary>
|
||||
public WaitHandlePool<MessageBase> WaitHandlePool { get; set; } = new();
|
||||
public WaitHandlePool<MessageBase> WaitHandlePool { get; set; } = new(0, ushort.MaxValue);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public WaitLock WaitLock => ChannelOptions.WaitLock;
|
||||
public virtual WaitLock GetLock(string key) => WaitLock;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ConcurrentDictionary<long, Func<IClientChannel, ReceivedDataEventArgs, bool, Task>> ChannelReceivedWaitDict { get; } = new();
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Task<Result> CloseAsync(string msg, CancellationToken token)
|
||||
@@ -85,19 +100,14 @@ public class UdpSessionChannel : UdpSession, IClientChannel
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task ConnectAsync(int timeout = 3000, CancellationToken token = default)
|
||||
public Task ConnectAsync(CancellationToken token = default)
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
return;
|
||||
await StartAsync().ConfigureAwait(false);
|
||||
return EasyTask.CompletedTask; ;
|
||||
return StartAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void SetDataHandlingAdapter(DataHandlingAdapter adapter)
|
||||
{
|
||||
if (adapter is UdpDataHandlingAdapter udpDataHandlingAdapter)
|
||||
SetAdapter(udpDataHandlingAdapter);
|
||||
}
|
||||
|
||||
public CancellationToken ClosedToken => this.m_transport == null ? new CancellationToken(true) : this.m_transport.Token;
|
||||
private CancellationTokenSource m_transport;
|
||||
/// <inheritdoc/>
|
||||
@@ -186,20 +196,17 @@ public class UdpSessionChannel : UdpSession, IClientChannel
|
||||
/// <inheritdoc/>
|
||||
protected override async Task OnUdpReceived(UdpReceivedDataEventArgs e)
|
||||
{
|
||||
await base.OnUdpReceived(e).ConfigureAwait(false);
|
||||
var receivedTask = base.OnUdpReceived(e);
|
||||
if (!receivedTask.IsCompleted)
|
||||
await receivedTask.ConfigureAwait(false);
|
||||
|
||||
if (e.RequestInfo is MessageBase response)
|
||||
{
|
||||
if (ChannelReceivedWaitDict.TryRemove(response.Sign, out var func))
|
||||
{
|
||||
await func.Invoke(this, e, ChannelReceived.Count == 1).ConfigureAwait(false);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
if (e.Handled)
|
||||
return;
|
||||
|
||||
await this.OnChannelReceivedEvent(e, ChannelReceived).ConfigureAwait(false);
|
||||
var channelReceivedTask = this.OnChannelReceivedEvent(e, ChannelReceived);
|
||||
if (!channelReceivedTask.IsCompleted)
|
||||
await channelReceivedTask.ConfigureAwait(false);
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -207,7 +214,8 @@ public class UdpSessionChannel : UdpSession, IClientChannel
|
||||
{
|
||||
m_transport?.SafeCancel();
|
||||
m_transport?.SafeDispose();
|
||||
WaitHandlePool.SafeDispose();
|
||||
m_transport = null;
|
||||
WaitHandlePool?.CancelAll();
|
||||
base.SafetyDispose(disposing);
|
||||
}
|
||||
}
|
||||
|
@@ -10,20 +10,37 @@
|
||||
|
||||
using System.Text;
|
||||
|
||||
using ThingsGateway.NewLife.Collections;
|
||||
|
||||
namespace ThingsGateway.Foundation;
|
||||
|
||||
/// <summary>
|
||||
/// TCP/Serial适配器基类
|
||||
/// </summary>
|
||||
public class DeviceSingleStreamDataHandleAdapter<TRequest> : TcpCustomDataHandlingAdapter<TRequest> where TRequest : MessageBase, new()
|
||||
public class DeviceSingleStreamDataHandleAdapter<TRequest> : CustomDataHandlingAdapter<TRequest>, IDeviceDataHandleAdapter where TRequest : MessageBase, new()
|
||||
{
|
||||
private ILog logger;
|
||||
|
||||
public new ILog Logger
|
||||
{
|
||||
get => logger ?? base.Logger;
|
||||
set
|
||||
{
|
||||
if (value != logger && value != null)
|
||||
{
|
||||
logger = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc cref="DeviceSingleStreamDataHandleAdapter{TRequest}"/>
|
||||
public DeviceSingleStreamDataHandleAdapter()
|
||||
{
|
||||
CacheTimeoutEnable = true;
|
||||
SurLength = int.MaxValue;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool CanSendRequestInfo => true;
|
||||
|
||||
@@ -40,11 +57,18 @@ public class DeviceSingleStreamDataHandleAdapter<TRequest> : TcpCustomDataHandli
|
||||
public TRequest Request { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SetRequest(ISendMessage sendMessage, ref ValueByteBlock byteBlock)
|
||||
public void SetRequest(ISendMessage sendMessage)
|
||||
{
|
||||
if (IsSingleThread)
|
||||
{
|
||||
if (Request != null)
|
||||
{
|
||||
_requestPool.Return(Request);
|
||||
}
|
||||
}
|
||||
var request = GetInstance();
|
||||
request.Sign = sendMessage.Sign;
|
||||
request.SendInfo(sendMessage, ref byteBlock);
|
||||
request.SendInfo(sendMessage);
|
||||
Request = request;
|
||||
}
|
||||
|
||||
@@ -55,24 +79,24 @@ public class DeviceSingleStreamDataHandleAdapter<TRequest> : TcpCustomDataHandli
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override FilterResult Filter<TByteBlock>(ref TByteBlock byteBlock, bool beCached, ref TRequest request, ref int tempCapacity)
|
||||
protected override FilterResult Filter<TReader>(ref TReader byteBlock, bool beCached, ref TRequest request)
|
||||
{
|
||||
if (Logger?.LogLevel <= LogLevel.Trace)
|
||||
Logger?.Trace($"{ToString()}- Receive:{(IsHexLog ? byteBlock.AsSegmentTake().ToHexString(' ') : byteBlock.ToString(byteBlock.Position))}");
|
||||
Logger?.Trace($"{ToString()}- Receive:{(IsHexLog ? byteBlock.ToHexString(byteBlock.BytesRead, ' ') : byteBlock.ToString(byteBlock.BytesRead))}");
|
||||
|
||||
try
|
||||
{
|
||||
if (IsSingleThread)
|
||||
request = Request == null ? GetInstance() : Request;
|
||||
request = Request == null ? Request = GetInstance() : Request;
|
||||
else
|
||||
{
|
||||
if (!beCached)
|
||||
request = GetInstance();
|
||||
}
|
||||
|
||||
var pos = byteBlock.Position;
|
||||
var pos = byteBlock.BytesRead;
|
||||
|
||||
if (request.HeaderLength > byteBlock.CanReadLength)
|
||||
if (request.HeaderLength > byteBlock.BytesRemaining)
|
||||
{
|
||||
return FilterResult.Cache;//当头部都无法解析时,直接缓存
|
||||
}
|
||||
@@ -80,19 +104,18 @@ public class DeviceSingleStreamDataHandleAdapter<TRequest> : TcpCustomDataHandli
|
||||
//检查头部合法性
|
||||
if (request.CheckHead(ref byteBlock))
|
||||
{
|
||||
byteBlock.Position = pos;
|
||||
byteBlock.BytesRead = pos;
|
||||
if (request.BodyLength > MaxPackageSize)
|
||||
{
|
||||
request.OperCode = -1;
|
||||
request.ErrorMessage = $"Received BodyLength={request.BodyLength}, greater than the set MaxPackageSize={MaxPackageSize}";
|
||||
OnError(default, request.ErrorMessage, true, true);
|
||||
SetResult(request);
|
||||
Reset();
|
||||
Logger?.LogWarning($"{ToString()} {request.ErrorMessage}");
|
||||
return FilterResult.GoOn;
|
||||
}
|
||||
if (request.BodyLength + request.HeaderLength > byteBlock.CanReadLength)
|
||||
if (request.BodyLength + request.HeaderLength > byteBlock.BytesRemaining)
|
||||
{
|
||||
//body不满足解析,开始缓存,然后保存对象
|
||||
tempCapacity = request.BodyLength + request.HeaderLength;
|
||||
return FilterResult.Cache;
|
||||
}
|
||||
//if (request.BodyLength <= 0)
|
||||
@@ -101,56 +124,48 @@ public class DeviceSingleStreamDataHandleAdapter<TRequest> : TcpCustomDataHandli
|
||||
// request.BodyLength = byteBlock.Length;
|
||||
//}
|
||||
var headPos = pos + request.HeaderLength;
|
||||
byteBlock.Position = headPos;
|
||||
byteBlock.BytesRead = headPos;
|
||||
var result = request.CheckBody(ref byteBlock);
|
||||
if (result == FilterResult.Cache)
|
||||
{
|
||||
byteBlock.BytesRead = pos;
|
||||
if (Logger?.LogLevel <= LogLevel.Trace)
|
||||
Logger?.Trace($"{ToString()}-Received incomplete, cached message, current length:{byteBlock.Length} {request?.ErrorMessage}");
|
||||
tempCapacity = request.BodyLength + request.HeaderLength;
|
||||
Logger?.Trace($"{ToString()}-Received incomplete, cached message, need length:{request.HeaderLength + request.BodyLength} ,current length:{byteBlock.BytesRead + byteBlock.BytesRemaining} {request?.ErrorMessage}");
|
||||
request.OperCode = -1;
|
||||
}
|
||||
else if (result == FilterResult.GoOn)
|
||||
{
|
||||
var addLen = request.HeaderLength + request.BodyLength;
|
||||
byteBlock.Position = pos + (addLen > 0 ? addLen : 1);
|
||||
Logger?.Trace($"{ToString()}-{request?.ToString()}");
|
||||
byteBlock.BytesRead = pos + (addLen > 0 ? addLen : 1);
|
||||
if (Logger?.LogLevel <= LogLevel.Trace)
|
||||
Logger?.Trace($"{ToString()}-{request?.ToString()}");
|
||||
request.OperCode = -1;
|
||||
SetResult(request);
|
||||
}
|
||||
else if (result == FilterResult.Success)
|
||||
{
|
||||
var addLen = request.HeaderLength + request.BodyLength;
|
||||
byteBlock.Position = pos + (addLen > 0 ? addLen : 1);
|
||||
byteBlock.BytesRead = pos + (addLen > 0 ? addLen : 1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
byteBlock.Position = pos + 1;//移动游标
|
||||
byteBlock.BytesRead = pos + 1;//移动游标
|
||||
request.OperCode = -1;
|
||||
SetResult(request);
|
||||
return FilterResult.GoOn;//放弃解析
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger?.LogWarning(ex, $"{ToString()} Received parsing error");
|
||||
byteBlock.Position = byteBlock.Length;//移动游标
|
||||
byteBlock.BytesRead = byteBlock.BytesRead + byteBlock.BytesRemaining;//移动游标
|
||||
request.Exception = ex;
|
||||
request.OperCode = -1;
|
||||
SetResult(request);
|
||||
return FilterResult.GoOn;//放弃解析
|
||||
}
|
||||
}
|
||||
|
||||
private void SetResult(TRequest request)
|
||||
{
|
||||
if ((Owner as IClientChannel)?.WaitHandlePool?.TryGetDataAsync(request.Sign, out var waitDataAsync) == true)
|
||||
{
|
||||
waitDataAsync.SetResult(request);
|
||||
}
|
||||
}
|
||||
private static ObjectPool<TRequest> _requestPool { get; } = new ObjectPool<TRequest>();
|
||||
|
||||
/// <summary>
|
||||
/// 获取泛型实例。
|
||||
@@ -158,50 +173,45 @@ public class DeviceSingleStreamDataHandleAdapter<TRequest> : TcpCustomDataHandli
|
||||
/// <returns></returns>
|
||||
protected virtual TRequest GetInstance()
|
||||
{
|
||||
return new TRequest() { OperCode = -1, Sign = -1 };
|
||||
if (IsSingleThread)
|
||||
{
|
||||
var request = _requestPool.Get();
|
||||
request.OperCode = -1;
|
||||
request.Sign = -1;
|
||||
return request;
|
||||
}
|
||||
else
|
||||
{
|
||||
return new TRequest() { OperCode = -1, Sign = -1 };
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnReceivedSuccess(TRequest request)
|
||||
public override void SendInput<TWriter>(ref TWriter writer, in ReadOnlyMemory<byte> memory)
|
||||
{
|
||||
Request = null;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task PreviewSendAsync(ReadOnlyMemory<byte> memory, CancellationToken cancellationToken)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (Logger?.LogLevel <= LogLevel.Trace)
|
||||
Logger?.Trace($"{ToString()}- Send:{(IsHexLog ? memory.Span.ToHexString(' ') : (memory.Span.ToString(Encoding.UTF8)))}");
|
||||
|
||||
//发送
|
||||
await GoSendAsync(memory, cancellationToken).ConfigureAwait(false);
|
||||
writer.Write(memory.Span);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override async Task PreviewSendAsync(IRequestInfo requestInfo, CancellationToken cancellationToken)
|
||||
public override void SendInput<TWriter>(ref TWriter writer, IRequestInfo requestInfo)
|
||||
{
|
||||
if (!(requestInfo is ISendMessage sendMessage))
|
||||
{
|
||||
throw new Exception($"Unable to convert {nameof(requestInfo)} to {nameof(ISendMessage)}");
|
||||
}
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
var byteBlock = new ValueByteBlock(sendMessage.MaxLength);
|
||||
try
|
||||
var span = writer.GetSpan(sendMessage.MaxLength);
|
||||
sendMessage.Build(ref writer);
|
||||
if (Logger?.LogLevel <= LogLevel.Trace)
|
||||
{
|
||||
sendMessage.Build(ref byteBlock);
|
||||
if (Logger?.LogLevel <= LogLevel.Trace)
|
||||
Logger?.Trace($"{ToString()}- Send:{(IsHexLog ? byteBlock.Span.ToHexString(' ') : (byteBlock.Span.ToString(Encoding.UTF8)))}");
|
||||
//非并发主从协议
|
||||
if (IsSingleThread)
|
||||
{
|
||||
SetRequest(sendMessage, ref byteBlock);
|
||||
}
|
||||
await GoSendAsync(byteBlock.Memory, cancellationToken).ConfigureAwait(false);
|
||||
Logger?.Trace($"{ToString()}- Send:{(IsHexLog ? span.Slice(0, (int)writer.WrittenCount).ToHexString(' ') : (span.Slice(0, (int)writer.WrittenCount).ToString(Encoding.UTF8)))}");
|
||||
}
|
||||
finally
|
||||
//非并发主从协议
|
||||
if (IsSingleThread)
|
||||
{
|
||||
byteBlock.SafeDispose();
|
||||
SetRequest(sendMessage);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user