using NewLife;
using NewLife.Caching;
using NewLife.Log;
using NewLife.Remoting;
using NewLife.Remoting.Extensions.Services;
using NewLife.Remoting.Models;
using NewLife.Security;
using NewLife.Serialization;
using Stardust.Data;
using Stardust.Data.Configs;
using Stardust.Data.Nodes;
using Stardust.Data.Platform;
using Stardust.Models;
using XCode;
using Service = Stardust.Data.Service;
namespace Stardust.Server.Services;
/// <summary>注册服务。处理应用注册、登录认证、服务注册与发现的核心服务</summary>
public class RegistryService : DefaultDeviceService<Node, NodeOnline>
{
private readonly AppOnlineService _appOnline;
private readonly IPasswordProvider _passwordProvider;
private readonly AppSessionManager _sessionManager;
private readonly ICacheProvider _cacheProvider;
private readonly StarServerSetting _setting;
private readonly ITracer _tracer;
/// <summary>实例化注册服务</summary>
/// <param name="appOnline">应用在线服务</param>
/// <param name="passwordProvider">密码提供者</param>
/// <param name="sessionManager">会话管理器</param>
/// <param name="cacheProvider">缓存提供者</param>
/// <param name="setting">服务端设置</param>
/// <param name="tracer">跟踪器</param>
/// <param name="serviceProvider">服务提供者</param>
public RegistryService(AppOnlineService appOnline, IPasswordProvider passwordProvider, AppSessionManager sessionManager, ICacheProvider cacheProvider, StarServerSetting setting, ITracer tracer, IServiceProvider serviceProvider) : base(sessionManager, passwordProvider, cacheProvider, serviceProvider)
{
_appOnline = appOnline;
_passwordProvider = passwordProvider;
_sessionManager = sessionManager;
_cacheProvider = cacheProvider;
_setting = setting;
_tracer = tracer;
Name = "App";
}
#region 登录注销
//public override ILoginResponse Login(DeviceContext context, ILoginRequest request, String source)
//{
// var rs = base.Login(context, request, source);
// if (context.Online is AppOnline online && request is AppModel inf)
// {
// // 关联节点,根据NodeCode匹配,如果未匹配上,则在未曾关联节点时才使用IP匹配
// var node = Node.FindByCode(inf.NodeCode);
// if (node == null && online.NodeId == 0) node = Node.SearchByIP(inf.IP).FirstOrDefault();
// if (node != null) online.NodeId = node.ID;
// if (!inf.Version.IsNullOrEmpty()) online.Version = inf.Version;
// var compile = inf.Compile.ToDateTime().ToLocalTime();
// if (compile.Year > 2000) online.Compile = compile;
// online.Update();
// }
// return rs;
//}
/// <summary>验证设备合法性</summary>
public override Boolean Authorize(DeviceContext context, ILoginRequest request)
{
if (context.Device is not App app) return false;
using var span = _tracer?.NewSpan($"{Name}Authorize", new { request.Code, request.ClientId });
var ip = context.UserHost;
// 检查黑白名单
if (!app.MatchIp(ip))
throw new ApiException(ApiCode.Forbidden, L.T("Application [{0}] forbids access from {1}.", app.Name, ip));
if (app.Project != null && !app.Project.MatchIp(ip))
throw new ApiException(ApiCode.Forbidden, L.T("Project [{0}] forbids access from {1}.", app.Project, ip));
// 检查应用有效性
if (!app.Enable) throw new ApiException(ApiCode.Forbidden, L.T("Application [{0}] is disabled.", app.Name));
// 安全治理:禁止空密钥应用匿名登录(认证绕过)。空密钥应用需在注册中心设置密钥,或临时开启"允许重新注册"自动收敛
if (app.Secret.IsNullOrEmpty())
{
app.WriteHistory(L.T("Application authentication"), false, L.T("An application with an empty secret cannot log in. Set a Secret for this application in the registry or enable re-registration."), null, ip, context.ClientId);
return false;
}
if (app.Secret.EqualIgnoreCase(request.Secret)) return true;
if (_setting.SaltTime > 0 && _passwordProvider is SaltPasswordProvider saltProvider)
{
// 使用盐值偏差时间,允许客户端时间与服务端时间有一定偏差
saltProvider.SaltTime = _setting.SaltTime;
}
if (request.Secret.IsNullOrEmpty() || !_passwordProvider.Verify(app.Secret, request.Secret))
{
app.WriteHistory(L.T("Application authentication"), false, L.T("Secret validation failed."), null, ip, context.ClientId);
return false;
}
return true;
}
/// <summary>自动注册</summary>
/// <param name="context"></param>
/// <param name="request"></param>
/// <returns></returns>
/// <exception cref="ApiException"></exception>
public override IDeviceModel Register(DeviceContext context, ILoginRequest request)
{
using var span = _tracer?.NewSpan($"{Name}Register", new { request.Code, request.ClientId });
var name = request.Code;
App app = null;
try
{
// 空应用标识直接拒绝:GetOrAdd 内部对空键直接解引用,会以 NRE 形式返回 500
if (name.IsNullOrEmpty()) throw new ApiException(ApiCode.BadRequest, L.T("Application identifier cannot be empty."));
// 查找应用。若已存在,说明客户端密钥校验失败(错误或为空)后被框架转入动态注册流程。
// 默认禁止复用旧身份自动放行,防止身份冒用与真实密钥泄露(安全扫描 V-1)
app = App.FindByName(name);
if (app != null)
{
// 按重注册模式放行:Off=拒绝;Auto=收敛窗口;Trusted=可信接入(同项目节点/近期指纹);Open=开放
var mode = app.ReRegister;
if (mode == ReRegisterModes.Off || (mode == ReRegisterModes.Trusted && !IsTrustedClient(app, request)))
throw new ApiException(ApiCode.Forbidden, L.T("Application [{0}] already exists; reusing the old identity for dynamic registration is forbidden. Check the secret or contact the administrator. To enable automatic instance onboarding, adjust the application's re-registration mode (convergence window/trusted access/open).", name));
var model = request as AppModel;
var uuid = (model != null && !model.UUID.IsNullOrEmpty()) ? model.UUID : null;
var remark = uuid == null
? L.T("[{0}] re-registered in [{1}] mode; secret issued", app.Name, app.ReRegister)
: L.T("[{0}] re-registered in [{1}] mode; secret issued, UUID={2}", app.Name, app.ReRegister, uuid);
app.WriteHistory(L.T("Application re-registered"), true, remark, null, context.UserHost, context.ClientId);
context.Device = app;
return app;
}
// 新应用,查找或创建应用,避免多线程创建冲突
app = App.GetOrAdd(name, App.FindByName, k => new App
{
Name = name,
Secret = Rand.NextString(16),
Enable = _setting.AppAutoRegister,
});
app.WriteHistory(L.T("Application registered"), true, L.T("[{0}] registered successfully.", app.Name), null, context.UserHost, context.ClientId);
context.Device = app;
}
catch (Exception ex)
{
span?.SetError(ex, null);
app?.WriteHistory(L.T("Application registered"), false, L.T("[{0}] registration failed: {1}", app.Name, ex.Message), null, context.UserHost, context.ClientId);
throw;
}
return app;
}
/// <summary>可信接入判定。应用实例声明运行于同项目受管节点(NodeCode),或UUID/Macs命中该应用在线指纹时视为可信</summary>
/// <param name="app">已存在应用</param>
/// <param name="request">登录请求</param>
/// <returns>是否可信</returns>
private Boolean IsTrustedClient(App app, ILoginRequest request)
{
if (request is not AppModel inf) return false;
// 节点门:节点存在、启用且与应用同项目(应用未归属项目时不限制),视为平台认识的受管机器
if (!inf.NodeCode.IsNullOrEmpty())
{
var node = Node.FindByCode(inf.NodeCode);
if (node != null && node.Enable && (app.ProjectId <= 0 || node.ProjectId == app.ProjectId))
{
XTrace.WriteLine("Trusted access for application [{0}]: matched node [{1}] in the same project, IP={2}", app, node, inf.IP);
return true;
}
}
// 指纹门:UUID/Macs 命中该应用在线会话(同机器或 Macs 交集),覆盖重装/密钥丢失后回归
if (!inf.UUID.IsNullOrEmpty() || !inf.Macs.IsNullOrEmpty())
{
if (AppOnline.IsRecentFingerprint(app.Id, inf.UUID, inf.Macs))
{
XTrace.WriteLine("Trusted access for application [{0}]: matched online fingerprint UUID={1}, IP={2}", app, inf.UUID, inf.IP);
return true;
}
}
return false;
}
/// <summary>鉴权后的登录处理。修改设备信息、创建在线记录和写日志</summary>
/// <param name="context">上下文</param>
/// <param name="request">登录请求</param>
public override void OnLogin(DeviceContext context, ILoginRequest request)
{
if (context.Device is not App app) return;
using var span = _tracer?.NewSpan($"{Name}OnLogin", new { request.Code, request.ClientId });
var model = request as AppModel;
var ip = context.UserHost;
// 设置默认项目
if (app.ProjectId == 0 || app.ProjectName == "默认")
{
var project = GalaxyProject.FindByName(model.Project);
if (project != null)
app.ProjectId = project.Id;
}
if (app.DisplayName.IsNullOrEmpty()) app.DisplayName = model.AppName;
// 比较编译时间,只要最新的
var compile = model.Compile.ToDateTime().ToLocalTime();
if (app.Compile < compile)
{
app.Compile = compile;
app.Version = model.Version;
}
if (app.DisplayName.IsNullOrEmpty()) app.DisplayName = model.AppName;
app.LastLogin = DateTime.Now;
app.LastIP = ip;
app.UpdateIP = ip;
// 密钥配对成功,仅收敛窗口模式(Auto)自动关闭重注册窗口,防止长期开启被冒用;可信接入/开放模式保持长期有效
if (app.ReRegister == ReRegisterModes.Auto && !app.Secret.IsNullOrEmpty()) app.ReRegister = ReRegisterModes.Off;
app.Update();
context.Online = GetOnline(context) ?? CreateOnline(context);
if (context.Online is AppOnline online && request is AppModel inf)
{
// 关联节点,根据NodeCode匹配,如果未匹配上,则在未曾关联节点时才使用IP匹配
var node = Node.FindByCode(inf.NodeCode);
if (node == null && online.NodeId == 0) node = Node.SearchByIP(inf.IP).FirstOrDefault();
if (node != null) online.NodeId = node.ID;
if (!inf.Version.IsNullOrEmpty()) online.Version = inf.Version;
//var compile = inf.Compile.ToDateTime().ToLocalTime();
if (compile.Year > 2000) online.Compile = compile;
// 记录机器指纹,供可信接入(Trusted)判定近期见过该机器
if (!inf.UUID.IsNullOrEmpty()) online.Uuid = inf.UUID;
if (!inf.Macs.IsNullOrEmpty()) online.Macs = inf.Macs;
online.Update();
}
// 登录历史
WriteHistory(context, L.T("Application authentication"), true, L.T("[{0}/{1}] authenticated {2}", app.DisplayName, app.Name, model.ToJson(false, false, false)));
// 检查应用上线恢复
OnlineService.CheckOnline(app);
}
/// <summary>注销</summary>
/// <param name="reason">注销原因</param>
/// <param name="ip">IP地址</param>
/// <returns></returns>
public override IOnlineModel? Logout(DeviceContext context, String? reason, String source)
{
//var online = appOnline.GetOnline(context.ClientId);
//if (online == null) return null;
//var app = context.Device as App;
//var msg = $"{reason} [{app}]]登录于{online.CreateTime},最后活跃于{online.UpdateTime}";
//app.WriteHistory("应用下线", true, msg, context.UserHost);
////!! 应用注销,不删除在线记录,保留在线记录用于查询
////online.Delete();
//appOnline.RemoveOnline(context.ClientId);
var online = base.Logout(context, reason, source);
if (online is AppOnline online2)
{
_appOnline.RemoveOnline(context.ClientId);
}
return online;
}
#endregion
#region 服务注册
public (AppService, Boolean changed) RegisterService(App app, Service service, PublishServiceInfo model, AppOnline online, String ip)
{
var clientId = model.ClientId;
var localIp = clientId;
if (!localIp.IsNullOrEmpty())
{
var p = localIp.IndexOf('@');
if (p > 0) localIp = localIp[..p];
}
// 单例部署服务,每个节点只有一个实例,使用本地IP作为唯一标识,无需进程ID,减少应用服务关联数
if (service.Singleton) clientId = localIp;
// 所有服务
var services = AppService.FindAllByService(service.Id);
var changed = false;
var svc = services.FirstOrDefault(e => e.AppId == app.Id && (e.Client == clientId || service.Singleton && !localIp.IsNullOrEmpty() && e.Client.StartsWith($"{localIp}@")));
if (svc == null)
{
svc = new AppService
{
AppId = app.Id,
ServiceId = service.Id,
ServiceName = model.ServiceName,
Client = clientId,
CreateIP = ip,
};
services.Add(svc);
changed = true;
WriteHistory(app, "RegisterService", true, L.T("Registered service [{0}] {1}", model.ServiceName, model.ClientId), ip, clientId);
}
else
{
if (!svc.Enable)
{
svc.Enable = app.AutoActive;
if (svc.Enable) changed = true;
}
}
// 节点信息
//var online = AppOnline.FindByClient(model.ClientId);
if (online != null) svc.NodeId = online.NodeId;
// 作用域
if (service.UseScope)
svc.Scope = AppRule.CheckScope(-1, ip, localIp);
svc.Enable = app.AutoActive;
svc.PingCount++;
svc.Tag = model.Tag;
svc.Version = model.Version;
svc.OriginAddress = model.Address;
svc.ExternalAddress = model.ExternalAddress;
// 地址处理。本地任意地址,更换为IP地址
var serverAddress = "";
if (service.Extranet && ip != "127.0.0.1" && ip != "::1")
{
serverAddress = ip;
}
else
{
serverAddress = model.IP;
if (serverAddress.IsNullOrEmpty()) serverAddress = localIp;
if (serverAddress.IsNullOrEmpty()) serverAddress = ip;
}
var urls = new List<String>();
foreach (var item in serverAddress.Split(","))
{
var addrs = model.Address
?.Replace("://*", $"://{item}")
?.Replace("://+", $"://{item}")
.Replace("://0.0.0.0", $"://{item}")
.Replace("://[::]", $"://{item}")
.Split(",");
if (addrs != null)
{
foreach (var elm in addrs)
{
var url = elm;
if (url.StartsWithIgnoreCase("http://", "https://")) url = new Uri(url).ToString().TrimEnd('/');
if (!urls.Contains(url)) urls.Add(url);
}
}
}
if (service.Address.IsNullOrEmpty())
{
if (!model.ExternalAddress.IsNullOrEmpty())
svc.Address = model.ExternalAddress?.Split(',').Take(5).Join(",");
else
svc.Address = urls.Take(5).Join(",");
}
else
{
// 地址模版
var addr = service.Address.Replace("{LocalIP}", localIp).Replace("{IP}", ip);
if (addr.Contains("{Port}"))
{
var port = 0;
foreach (var item in urls)
{
var p = item.IndexOf(":", "http://".Length);
if (p >= 0)
{
port = item[(p + 1)..].TrimEnd('/').ToInt();
if (port > 0) break;
}
}
if (port > 0) addr = addr.Replace("{Port}", port + "");
}
svc.Address = addr;
}
// 无需健康监测,直接标记为健康
if (!model.Health.IsNullOrEmpty()) service.HealthAddress = model.Health;
if (!service.HealthCheck || service.HealthAddress.IsNullOrEmpty()) svc.Healthy = true;
svc.Save();
service.Providers = services.Count;
service.Save();
// 如果有改变,异步监测健康状况
if (changed && service.HealthCheck && !service.HealthAddress.IsNullOrEmpty())
{
_ = Task.Run(() => HealthCheck(svc));
}
return (svc, changed);
}
/// <summary>服务健康检查。按服务的健康检测地址探测一次,并更新服务的健康状态</summary>
/// <param name="svc">应用服务实例</param>
public async Task HealthCheck(AppService svc)
{
var url = svc.Service?.HealthAddress;
if (url.IsNullOrEmpty()) return;
try
{
if (!url.StartsWithIgnoreCase("http://", "https://"))
{
var ss = svc.Address.Split(',');
var uri = new Uri(new Uri(ss[0]), url);
url = uri.ToString();
}
var http = _tracer.CreateHttpClient();
var rs = await http.GetStringAsync(url);
svc.Healthy = true;
svc.CheckResult = rs;
}
catch (Exception ex)
{
svc.Healthy = false;
svc.CheckResult = ex.ToString();
XTrace.WriteLine("HealthCheck: {0}", url);
XTrace.Log.Error(ex.Message);
}
svc.CheckTimes++;
svc.LastCheck = DateTime.Now;
svc.Update();
}
public (AppService, Boolean changed) UnregisterService(App app, Service info, PublishServiceInfo model, String ip)
{
// 单例部署服务,每个节点只有一个实例,使用本地IP作为唯一标识,无需进程ID,减少应用服务关联数
var clientId = model.ClientId;
if (info.Singleton && !clientId.IsNullOrEmpty())
{
var p = clientId.IndexOf('@');
if (p > 0) clientId = clientId[..p];
}
// 所有服务
var services = AppService.FindAllByService(info.Id);
var changed = false;
var svc = services.FirstOrDefault(e => e.AppId == app.Id && e.Client == clientId);
if (svc != null)
{
//svc.Delete();
svc.Enable = false;
svc.Healthy = false;
svc.Update();
services.Remove(svc);
changed = true;
WriteHistory(app, "UnregisterService", true, L.T("Service [{0}] went offline: {1}", model.ServiceName, svc.Client), ip, svc.Client);
}
info.Providers = services.Count;
info.Save();
return (svc, changed);
}
public ServiceModel[] ResolveService(Service service, ConsumeServiceInfo model, String scope)
{
var list = new List<ServiceModel>();
var tags = model.Tag?.Split(",");
// 该服务所有生产
var services = AppService.FindAllByService(service.Id);
foreach (var item in services)
{
// 启用,匹配规则,健康
if (item.Enable && item.Healthy && item.Match(model.MinVersion, scope, tags))
{
list.Add(item.ToModel());
}
}
return list.ToArray();
}
#endregion
#region 心跳保活
//public override IOnlineModel OnPing(DeviceContext context, IPingRequest request)
//{
// if (context.Device is not App app) return null;
// // 更新在线记录
// var inf = request as AppInfo;
// //var (online, _) = appOnline.GetOnline(app, context.ClientId, context.Token, inf?.IP, context.UserHost);
// var online = base.OnPing(context, request) as AppOnline;
// if (online != null)
// {
// //online.Version = app.Version;
// online.Fill(app, inf);
// online.SaveAsync();
// }
// //// 保存性能数据
// //AppMeter.WriteData(app, inf, "Ping", clientId, ip);
// return online;
//}
private static Int32 _totalCommands;
private static IList<AppCommand> _commands = [];
private static DateTime _nextTime;
public override CommandModel[]? AcquireCommands(DeviceContext context)
{
// 缓存最近1000个未执行命令,用于快速过滤,避免大量节点在线时频繁查询命令表
if (_nextTime < DateTime.Now || _totalCommands != AppCommand.Meta.Count)
{
_totalCommands = AppCommand.Meta.Count;
_commands = AppCommand.AcquireCommands(-1, 1000);
_nextTime = DateTime.Now.AddMinutes(1);
}
if (context.Device is not App app) return null;
var appId = app.Id;
// 是否有本节点
if (!_commands.Any(e => e.AppId == appId)) return null;
using var span = _tracer?.NewSpan(nameof(AcquireCommands), new { appId });
var cmds = AppCommand.AcquireCommands(appId, 100);
if (cmds.Count == 0) return null;
var rs = new List<CommandModel>();
foreach (var item in cmds)
{
// 命令要提前下发,在客户端本地做延迟处理,这里不应该过滤掉
//// 命令是否已经开始
//if (item.StartTime > DateTime.Now) continue;
// 带有过期时间的命令,加大重试次数
var maxTimes = item.Expire.Year > 2000 ? 100 : 10;
if (item.Times > maxTimes || item.Expire.Year > 2000 && item.Expire < DateTime.Now)
item.Status = CommandStatus.取消;
else
{
// 如果命令正在处理中,则短期内不重复下发。客户端StarAgent具备去重能力,不需要服务端过滤
//if (item.Status == CommandStatus.处理中 && item.UpdateTime.AddSeconds(30) > DateTime.Now) continue;
// 即时指令,或者已到开始时间的未来指令,才增加次数
if (item.StartTime.Year < 2000 || item.StartTime < DateTime.Now)
item.Times++;
item.Status = CommandStatus.处理中;
var commandModel = BuildCommand(item.App, item);
rs.Add(commandModel);
}
item.UpdateTime = DateTime.Now;
}
cmds.Update(false);
return rs.ToArray();
}
///// <summary>获取在线。先查缓存再查库</summary>
///// <param name="context">上下文</param>
///// <returns></returns>
//public override IOnlineModel GetOnline(DeviceContext context) => base.GetOnline(context) as AppOnline;
///// <summary>设置设备的长连接上线/下线</summary>
///// <param name="context">上下文</param>
///// <param name="online"></param>
///// <returns></returns>
//public override void SetOnline(DeviceContext context, Boolean online)
//{
// if ((GetOnline(context) ?? context.Online) is AppOnline olt)
// {
// olt.LongLink = online;
// olt.Update();
// }
//}
#endregion
#region 下行通知
/// <summary>向应用发送命令</summary>
/// <param name="app">应用</param>
/// <param name="clientId">应用实例标识。向特定应用实例发送命令时指定</param>
/// <param name="model">命令模型</param>
/// <param name="user">创建者</param>
/// <returns></returns>
public async Task<CommandReplyModel> SendCommand(App app, String clientId, CommandInModel model, String user, CancellationToken cancellationToken = default)
{
//if (model.Code.IsNullOrEmpty()) throw new ArgumentNullException(nameof(model.Code), L.T("Application must be specified."));
if (model.Command.IsNullOrEmpty()) throw new ArgumentNullException(nameof(model.Command));
var cmd = new AppCommand
{
AppId = app.Id,
Command = model.Command,
Argument = model.Argument,
//Expire = model.Expire,
TraceId = DefaultSpan.Current?.TraceId,
CreateUser = user,
};
if (model.StartTime > 0) cmd.StartTime = DateTime.Now.AddSeconds(model.StartTime);
if (model.Expire > 0) cmd.Expire = DateTime.Now.AddSeconds(model.Expire);
cmd.Insert();
// 分发命令给该应用的所有实例
var cmdModel = BuildCommand(app, cmd);
var ts = new List<Task<CommandReplyModel?>>();
foreach (var item in AppOnline.FindAllByApp(app.Id))
{
// 对特定实例发送
if (!clientId.IsNullOrEmpty() && item.Client != clientId) continue;
//_queue.Publish(app.Name, item.Client, cmdModel);
var code = $"{app.Name}@{item.Client}";
ts.Add(_sessionManager.PublishAsync(code, cmdModel, null, model.Timeout, cancellationToken));
}
// 目标应用没有任何在线实例(离线),无法下发命令,直接返回空,避免等待空任务抛空引用异常
if (ts.Count == 0) return null;
await Task.WhenAll(ts);
// SessionManager.PublishAsync 内置timeout等待,取首个非空响应。
// 即发即忘(timeout<=0)时所有任务结果均为空,FirstOrDefault 会返回空任务,不能直接 await
foreach (var item in ts)
{
if (item.Result != null) return item.Result;
}
return null;
}
public override Task<CommandReplyModel?> SendCommand(DeviceContext context, CommandInModel model, CancellationToken cancellationToken = default)
{
if (context.Device is not App app) return null;
return SendCommand(app, null, model, null);
}
public override Int32 CommandReply(DeviceContext context, CommandReplyModel model)
{
var app = context.Device as App;
var cmd = AppCommand.FindById((Int32)model.Id);
if (cmd == null) return 0;
// 防止越权
if (cmd.AppId != app.Id) throw new ApiException(ApiCode.Forbidden, L.T("[{0}] attempted to access the service of [{1}] without permission", app, cmd.AppName));
cmd.Status = model.Status;
cmd.Result = model.Data;
cmd.Update();
// 通过会话管理器内置的响应事件总线广播响应(跨实例广播不阻塞)
_ = _sessionManager.PublishResponseAsync(model, default);
return 1;
}
/// <summary>通知该服务的所有消费者,服务信息有变更</summary>
/// <param name="service"></param>
/// <param name="command"></param>
/// <param name="user"></param>
public async Task NotifyConsumers(AppService service, String command, String user = null)
{
var list = AppConsume.FindAllByService(service.ServiceId);
if (list.Count == 0) return;
// 获取所有订阅该服务的应用,可能相同应用多实例订阅,需要去重
var appIds = list.Select(e => e.AppId).Distinct().ToArray();
var arguments = new { service.AppName, service.ServiceName, service.Address }.ToJson();
using var span = _tracer?.NewSpan(nameof(NotifyConsumers), $"{command} appIds={appIds.Join()} user={user} arguments={arguments}");
var ts = new List<Task>();
foreach (var item in appIds)
{
var app = App.FindById(item);
if (app != null)
{
var model = new CommandInModel
{
Command = command,
Argument = arguments,
Expire = 600,
};
ts.Add(SendCommand(app, null, model, user));
}
}
await Task.WhenAll(ts);
}
//private static Version _version = new(3, 1, 2025, 0103);
private CommandModel BuildCommand(App app, AppCommand cmd)
{
var model = cmd.ToModel();
model.TraceId = DefaultSpan.Current + "";
// 新版本使用UTC时间
if (app.Compile.Year >= 2025)
{
if (model.StartTime.Year > 2000)
model.StartTime = model.StartTime.ToUniversalTime();
if (model.Expire.Year > 2000)
model.Expire = model.Expire.ToUniversalTime();
}
return model;
}
#endregion
#region 事件上报
//public Int32 PostEvents(DeviceContext context, EventModel[] events)
//{
// var app = context.Device as App;
// var ip = context.UserHost;
// var olt = AppOnline.FindByClient(clientId);
// var his = new List<AppHistory>();
// foreach (var model in events)
// {
// //WriteHistory(model.Name, !model.Type.EqualIgnoreCase("error"), model.Time.ToDateTime().ToLocalTime(), model.Remark, null);
// var success = !model.Type.EqualIgnoreCase("error");
// var time = model.Time.ToDateTime().ToLocalTime();
// var hi = AppHistory.Create(app, model.Name, success, model.Remark, olt?.Version, Environment.MachineName, ip);
// hi.Client = clientId;
// if (time.Year > 2000) hi.CreateTime = time;
// his.Add(hi);
// }
// his.Insert();
// return events.Length;
//}
protected override IEntity CreateEvent(DeviceContext context, IDeviceModel2 device, EventModel model)
{
var entity = base.CreateEvent(context, device, model);
if (entity is AppHistory history)
{
var online = GetOnline(context) as AppOnline;
history.Version = online?.Version;
history.Client = online?.Client;
history.NodeId = online?.NodeId ?? 0;
var time = model.Time.ToDateTime().ToLocalTime();
if (time.Year > 2000) history.CreateTime = time;
}
return entity;
}
#endregion
#region 辅助
public override IDeviceModel? QueryDevice(String code) => App.FindByName(code);
public override IOnlineModel? QueryOnline(String sessionId) => AppOnline.FindBySessionId(sessionId, true);
protected override String GetSessionId(DeviceContext context) => context.ClientId ?? base.GetSessionId(context);
private void WriteHistory(App app, String action, Boolean success, String remark, String ip, String clientId)
{
var online = AppOnline.FindBySessionId(clientId, true);
var version = online?.Version;
var history = AppHistory.Create(app, action, success, remark, version, Environment.MachineName, ip);
history.Client = clientId;
history.NodeId = online?.NodeId ?? 0;
history.Insert();
}
/// <summary>写设备历史</summary>
/// <param name="context">上下文</param>
/// <param name="action">动作</param>
/// <param name="success">成功</param>
/// <param name="remark">备注内容</param>
public override void WriteHistory(DeviceContext context, String action, Boolean success, String remark)
{
var online = context.Online as AppOnline;
var version = online?.Version;
var history = AppHistory.Create(context.Device as App, action, success, remark, version, Environment.MachineName, context.UserHost);
history.Client = context.ClientId;
history.NodeId = online?.NodeId ?? 0;
history.Insert();
}
#endregion
}
|