星尘服务端,采用asp.net core 的 webapi

This commit is contained in:
大石头 2020-03-12 23:23:25 +08:00
parent ecbff49b9c
commit af6b815cb5
21 changed files with 1613 additions and 121 deletions

112
.editorconfig Normal file
View File

@ -0,0 +1,112 @@
# EditorConfig is awesome:http://EditorConfig.org
# https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference
# top-most EditorConfig file
root = true
# Don't use tabs for indentation.
[*]
indent_style = space
# (Please don't specify an indent_size here; that has too many unintended consequences.)
# Code files
[*.{cs,csx,vb,vbx}]
indent_size = 4
insert_final_newline = false
charset = utf-8-bom
end_of_line = crlf
# Xml project files
[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]
indent_size = 2
# Xml config files
[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
indent_size = 2
# JSON files
[*.json]
indent_size = 2
# Dotnet code style settings:
[*.{cs,vb}]
# Sort using and Import directives with System.* appearing first
dotnet_sort_system_directives_first = true
csharp_indent_case_contents = true
csharp_indent_switch_labels = true
csharp_indent_labels = flush_left
#csharp_space_after_cast = true
#csharp_space_after_keywords_in_control_flow_statements = true
#csharp_space_between_method_declaration_parameter_list_parentheses = true
#csharp_space_between_method_call_parameter_list_parentheses = true
#csharp_space_between_parentheses = control_flow_statements, type_casts
# 单行放置代码
csharp_preserve_single_line_statements = true
csharp_preserve_single_line_blocks = true
# Avoid "this." and "Me." if not necessary
dotnet_style_qualification_for_field = false:warning
dotnet_style_qualification_for_property = false:warning
dotnet_style_qualification_for_method = false:warning
dotnet_style_qualification_for_event = false:warning
# Use language keywords instead of framework type names for type references
dotnet_style_predefined_type_for_locals_parameters_members = false:suggestion
dotnet_style_predefined_type_for_member_access = false:suggestion
#dotnet_style_require_accessibility_modifiers = for_non_interface_members:none/always:suggestion
# Suggest more modern language features when available
dotnet_style_object_initializer = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_explicit_tuple_names = true:suggestion
dotnet_style_prefer_inferred_tuple_names = true:suggestion
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
# CSharp code style settings:
[*.cs]
# Prefer "var" everywhere
csharp_style_var_for_built_in_types = true:warning
csharp_style_var_when_type_is_apparent = true:warning
csharp_style_var_elsewhere = true:warning
# Prefer method-like constructs to have a block body
csharp_style_expression_bodied_methods = when_on_single_line:suggestion
csharp_style_expression_bodied_constructors = when_on_single_line:suggestion
csharp_style_expression_bodied_operators = when_on_single_line:suggestion
# Prefer property-like constructs to have an expression-body
csharp_style_expression_bodied_properties = true:suggestion
csharp_style_expression_bodied_indexers = true:suggestion
#csharp_style_expression_bodied_accessors = true:suggestion
# Suggest more modern language features when available
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
csharp_prefer_simple_default_expression = true:suggestion
csharp_style_deconstructed_variable_declaration = true:suggestion
csharp_style_pattern_local_over_anonymous_function = true:suggestion
csharp_style_throw_expression = true:suggestion
csharp_style_conditional_delegate_call = true:suggestion
# 单行不需要大括号
csharp_prefer_braces = false:suggestion
# Newline settings
csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true
csharp_new_line_before_catch = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_between_query_expression_clauses = true
[*.md]
trim_trailing_whitespace = false

View File

@ -0,0 +1,54 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using NewLife.Log;
using System;
using System.Linq;
namespace Stardust.Server.Common
{
/// <summary>统一Api过滤处理</summary>
public sealed class ApiFilterAttribute : ActionFilterAttribute
{
/// <summary>执行前,验证模型</summary>
/// <param name="context"></param>
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
throw new ApplicationException(context.ModelState.Values.First(p => p.Errors.Count > 0).Errors[0].ErrorMessage);
base.OnActionExecuting(context);
}
/// <summary>执行后,包装结果和异常</summary>
/// <param name="context"></param>
public override void OnActionExecuted(ActionExecutedContext context)
{
if (context.Result != null)
{
if (context.Result is ObjectResult obj)
{
context.Result = new JsonResult(new { code = 0, data = obj.Value });
}
else if (context.Result is EmptyResult)
{
context.Result = new JsonResult(new { code = 0, data = new { } });
}
}
else if (context.Exception != null && !context.ExceptionHandled)
{
var ex = context.Exception.GetTrue();
if (ex is NewLife.Remoting.ApiException aex)
context.Result = new JsonResult(new { code = aex.Code, data = aex.Message });
else
context.Result = new JsonResult(new { code = 500, data = ex.Message });
context.ExceptionHandled = true;
// 输出异常日志
if (XTrace.Debug) XTrace.WriteException(ex);
}
base.OnActionExecuted(context);
}
}
}

View File

@ -0,0 +1,15 @@
using System;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Stardust.Server.Common
{
public class DateTimeConverter : JsonConverter<DateTime>
{
public String DateTimeFormat { get; set; } = "yyyy-MM-dd HH:mm:ss";
public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => DateTime.Parse(reader.GetString());
public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) => writer.WriteStringValue(value.ToString(DateTimeFormat));
}
}

View File

@ -0,0 +1,23 @@
using System;
using Microsoft.AspNetCore.Mvc.Filters;
using NewLife.Remoting;
using Stardust.Server.Controllers;
namespace Stardust.Server.Common
{
/// <summary>令牌校验</summary>
public class TokenFilterAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (context.Controller is BaseController bc)
{
var session = bc.Session;
if (bc.Token.IsNullOrEmpty()) throw new ApiException(403, "未授权");
if (session == null) throw new ApiException(402, "令牌无效");
}
base.OnActionExecuting(context);
}
}
}

View File

@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NewLife.Caching;
using NewLife.Collections;
namespace Stardust.Server.Common
{
public class TokenSession
{
/// <summary>有效期</summary>
public TimeSpan Expire { get; set; } = TimeSpan.FromMinutes(20);
/// <summary>会话存储</summary>
public ICache Cache { get; set; } = new MemoryCache { Expire = 20 * 60, Period = 60 * 10 };
/// <summary>创建新的Session</summary>
/// <param name="token"></param>
/// <returns></returns>
public IDictionary<String, Object> CreateSession(String token)
{
var key = GetKey(token);
var dic = Cache.GetDictionary<Object>(key);
Cache.SetExpire(key, Expire);
//!! 临时修正可空字典的BUG
if (Cache is MemoryCache mc)
{
dic = new NullableDictionary<String, Object>();
mc.Set(key, dic);
}
return dic;
}
/// <summary>根据Token获取session</summary>
/// <param name="token"></param>
/// <returns></returns>
public IDictionary<String, Object> GetSession(String token)
{
if (token.IsNullOrEmpty()) return null;
var key = GetKey(token);
// 当前缓存没有指定的token 则直接返回null
if (!Cache.ContainsKey(key)) return null;
// 采用哈希结构。内存缓存用并行字段Redis用Set
var dic = Cache.GetDictionary<Object>(key);
Cache.SetExpire(key, Expire);
return dic;
}
/// <summary>
/// 刷新token有效期
/// </summary>
/// <param name="token"></param>
/// <param name="newToken"></param>
public IDictionary<String, Object> CopySession(String token, String newToken)
{
if (newToken.IsNullOrEmpty()) return null;
var dic = GetSession(token);
if (dic == null) return null;
var nkey = GetKey(newToken);
if (Cache is MemoryCache mc)
{
mc.Set(nkey, dic, Expire);
}
//else if (Cache is Redis rds)
//{
// // redis.rename
//}
else
{
//var ndic = Cache.GetDictionary<Object>(nkey);
var ndic = CreateSession(newToken);
foreach (var item in dic)
{
ndic[item.Key] = item.Value;
}
}
// 确保建立新的
return GetSession(newToken);
}
///// <summary>修复session服务器切换或机房切换情况</summary>
///// <returns></returns>
//public IDictionary<String, Object> FixSession(String token)
//{
// if (token.IsNullOrEmpty()) return null;
// var key = GetKey(token);
// // 采用哈希结构。内存缓存用并行字段Redis用Set
// var dic = Cache.GetDictionary<Object>(key);
// Cache.SetExpire(key, Expire);
// return dic;
//}
/// <summary>根据令牌活期缓存Key</summary>
/// <param name="token"></param>
/// <returns></returns>
protected virtual String GetKey(String token) => (!token.IsNullOrEmpty() && token.Length > 16) ? token.MD5() : token;
}
}

View File

@ -0,0 +1,36 @@
using Microsoft.AspNetCore.Http;
using System;
namespace Stardust.Server.Common
{
/// <summary>Web助手</summary>
public static class WebHelper
{
/// <summary>获取用户主机</summary>
/// <param name="context"></param>
/// <returns></returns>
public static String GetUserHost(this HttpContext context)
{
var request = context.Request;
var str = "";
if (str.IsNullOrEmpty()) str = request.Headers["HTTP_X_FORWARDED_FOR"];
if (str.IsNullOrEmpty()) str = request.Headers["X-Real-IP"];
if (str.IsNullOrEmpty()) str = request.Headers["X-Forwarded-For"];
if (str.IsNullOrEmpty()) str = request.Headers["REMOTE_ADDR"];
//if (str.IsNullOrEmpty()) str = request.Headers["Host"];
//if (str.IsNullOrEmpty()) str = context.Connection?.RemoteIpAddress?.MapToIPv4() + "";
if (str.IsNullOrEmpty())
{
var addr = context.Connection?.RemoteIpAddress;
if (addr != null)
{
if (addr.IsIPv4MappedToIPv6) addr = addr.MapToIPv4();
str = addr + "";
}
}
return str;
}
}
}

View File

@ -0,0 +1,86 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using System;
namespace Stardust.Server.Common
{
static class XLoggerExtensions
{
public static ILoggingBuilder AddXLog(this ILoggingBuilder builder)
{
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, XLoggerProvider>());
return builder;
}
}
[ProviderAlias("XLog")]
class XLoggerProvider : ILoggerProvider
{
public ILogger CreateLogger(String categoryName)
{
var log = NewLife.Log.XTrace.Log;
if (log is NewLife.Log.CompositeLog cp)
{
var tf = cp.Get<NewLife.Log.TextFileLog>();
if (tf != null) log = tf;
}
return new XLogger { Logger = log };
}
public void Dispose() { }
}
class XLogger : ILogger
{
public NewLife.Log.ILog Logger { get; set; }
public IDisposable BeginScope<TState>(TState state)
{
return null;
}
public Boolean IsEnabled(LogLevel logLevel)
{
return Logger.Enable;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, String> formatter)
{
if (!Logger.Enable) return;
if (formatter == null) throw new ArgumentNullException(nameof(formatter));
var txt = formatter(state, exception);
if (txt.IsNullOrEmpty() && exception == null) return;
switch (logLevel)
{
case LogLevel.Trace:
case LogLevel.Debug:
Logger.Debug(txt);
break;
case LogLevel.Information:
Logger.Info(txt);
break;
case LogLevel.Warning:
Logger.Warn(txt);
break;
case LogLevel.Error:
Logger.Error(txt);
break;
case LogLevel.Critical:
Logger.Fatal(txt);
break;
case LogLevel.None:
break;
default:
break;
}
if (exception != null) Logger.Error("{0}", exception);
}
}
}

View File

@ -0,0 +1,139 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Net.Http.Headers;
using NewLife.Data;
using NewLife.Reflection;
using NewLife.Serialization;
using Stardust.Server.Common;
namespace Stardust.Server.Controllers
{
[ApiController]
[Route("[controller]")]
public class ApiController : ControllerBase
{
/// <summary>获取所有接口</summary>
/// <returns></returns>
[ApiFilter]
[HttpGet]
public Object Get() => Info(null);
private static readonly String _OS = Environment.OSVersion + "";
private static readonly String _MachineName = Environment.MachineName;
private static readonly String _UserName = Environment.UserName;
private static readonly String _LocalIP = NetHelper.MyIP() + "";
/// <summary>服务器信息,用户健康检测</summary>
/// <param name="state">状态信息</param>
/// <returns></returns>
[ApiFilter]
[HttpGet(nameof(Info))]
public Object Info(String state)
{
var conn = HttpContext.Connection;
var asmx = AssemblyX.Entry;
var asmx2 = AssemblyX.Create(Assembly.GetExecutingAssembly());
var ip = HttpContext.GetUserHost();
var rs = new
{
Server = asmx?.Name,
asmx?.FileVersion,
asmx?.Compile,
OS = _OS,
MachineName = _MachineName,
UserName = _UserName,
ApiVersion = asmx2?.Version,
UserHost = HttpContext.GetUserHost(),
LocalIP = _LocalIP,
Remote = ip + "",
State = state,
//LastState = Session?["State"],
Time = DateTime.Now,
};
//// 记录上一次状态
//if (Session != null) Session["State"] = state;
// 转字典
var dic = rs.ToDictionary();
//// 令牌
//if (!Token.IsNullOrEmpty()) dic["Token"] = Token;
// 时间和连接数
//if (Host is ApiHost ah) dic["Uptime"] = (DateTime.Now - ah.StartTime).ToString();
dic["Port"] = conn.LocalPort;
//dic["Online"] = nsvr.SessionCount;
//dic["MaxOnline"] = nsvr.MaxSessionCount;
// 进程
dic["Process"] = GetProcess();
//// 加上统计信息
//dic["Stat"] = GetStat();
return dic;
}
private Object GetProcess()
{
var proc = Process.GetCurrentProcess();
return new
{
Environment.ProcessorCount,
ProcessId = proc.Id,
Threads = proc.Threads.Count,
Handles = proc.HandleCount,
WorkingSet = proc.WorkingSet64,
PrivateMemory = proc.PrivateMemorySize64,
GCMemory = GC.GetTotalMemory(false),
GC0 = GC.GetGeneration(0),
GC1 = GC.GetGeneration(1),
GC2 = GC.GetGeneration(2),
};
}
private static Packet _myInfo;
/// <summary>服务器信息,用户健康检测,二进制压测</summary>
/// <param name="state">状态信息</param>
/// <returns></returns>
[HttpPost(nameof(Info2))]
public async Task<ObjectResult> Info2()
{
if (_myInfo == null)
{
// 不包含时间和远程地址
var rs = new
{
MachineNam = _MachineName,
UserName = _UserName,
LocalIP = _LocalIP,
};
_myInfo = new Packet(rs.ToJson().GetBytes());
}
var buf = new Byte[4096];
var count = await Request.Body.ReadAsync(buf, 0, buf.Length);
var state = new Packet(buf, 0, count);
var pk = _myInfo.Slice(0, -1);
pk.Append(state);
var res = new ObjectResult(pk.GetStream());
res.Formatters.Add(new StreamOutputFormatter());
res.ContentTypes.Add(new MediaTypeHeaderValue("application/octet-stream"));
return res;
}
}
}

View File

@ -0,0 +1,182 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using NewLife.Log;
using NewLife.Web;
using Stardust.Data.Nodes;
using Stardust.Server.Common;
using XCode.Membership;
namespace Stardust.Server.Controllers
{
public abstract class BaseController : ControllerBase, IActionFilter
{
#region
/// <summary></summary>
static readonly TokenSession _session = new TokenSession();
/// <summary>临时会话扩展信息</summary>
public IDictionary<String, Object> Session { get; private set; }
/// <summary>令牌</summary>
public String Token { get; private set; }
/// <summary>是否使用Cookie</summary>
public Boolean UseCookies { get; set; }
/// <summary>用户主机</summary>
public String UserHost => HttpContext.GetUserHost();
#endregion
#region
/// <summary>请求处理后</summary>
/// <param name="context"></param>
public void OnActionExecuted(ActionExecutedContext context)
{
if (context.Exception != null)
{
// 拦截全局异常,写日志
var action = context.HttpContext.Request.Path + "";
if (context.ActionDescriptor is ControllerActionDescriptor act) action = $"{act.ControllerName}/{act.ActionName}";
var node = Session != null ? Session["Node"] as Node : null;
WriteHistory(node, action, false, context.Exception?.GetTrue() + "");
}
}
/// <summary>请求处理前</summary>
/// <param name="context"></param>
public void OnActionExecuting(ActionExecutingContext context)
{
var ip = UserHost;
ManageProvider.UserHost = ip;
var request = context.HttpContext.Request;
var token = request.Query["Token"] + "";
if (token.IsNullOrEmpty()) token = (request.Headers["Authorization"] + "").TrimStart("Bearer ");
if (token.IsNullOrEmpty()) token = request.Headers["X-Token"] + "";
if (token.IsNullOrEmpty()) token = request.Cookies["Token"] + "";
if (!token.IsNullOrEmpty())
{
Token = token;
Session = _session.GetSession(token);
// 考虑到可能出现的服务器切换或服务端闪断等情况
if (Session == null) Session = RestoreSession(token);
}
else
{
CreateToken(null);
}
}
#endregion
#region
private static TokenProvider _tokenProvider;
private static TokenProvider GetTokenProvider()
{
if (_tokenProvider != null) return _tokenProvider;
var provider = new TokenProvider();
provider.ReadKey("../keys/token.prvkey", false);
if (provider.Key.IsNullOrEmpty()) throw new ApplicationException("缺失私钥,无法创建令牌");
return _tokenProvider = provider;
}
/// <summary>刷新令牌</summary>
/// <param name="newTokedn"></param>
protected void RefreshToken(String newToken)
{
Session = _session.CopySession(Token, newToken);
Token = newToken;
}
/// <summary>创建token同时刷新token有效期</summary>
/// <param name="code"></param>
protected void CreateToken(String code)
{
var set = Setting.Current;
var expire = set.TokenExpire;
var provider = GetTokenProvider();
var token = provider.Encode(code, DateTime.Now.AddSeconds(expire));
if (Token.IsNullOrEmpty())
{
// 创建新的token
Session = _session.CreateSession(token);
Token = token;
}
else
{
RefreshToken(token);
}
if (UseCookies) Response.Cookies.Append("Token", Token);
}
/// <summary>由token恢复session</summary>
/// <param name="token"></param>
private IDictionary<String, Object> RestoreSession(String token)
{
if (token.IsNullOrEmpty()) return null;
var str = token.Trim().Substring(null, ".")?.ToBase64().ToStr();
var rlist = str?.Split('#', ',');
if (rlist == null)
{
XTrace.WriteLine($"Token 解析失败!:{token}");
return null;
}
var code = rlist[0];
var node = Node.FindByCode(code) ?? new Node { Code = code };
var provider = GetTokenProvider();
// token解码失败
if (!provider.TryDecode(token, out var result, out var dt))
{
var msg = $"签名错误:{result ?? "null"} {dt:yyyy-MM-dd HH:mm:ss} {token}";
XTrace.WriteLine(msg);
WriteHistory(node, "签名错误", false, msg);
return null;
}
// token 过期
if (dt < DateTime.Now)
{
var msg = $"令牌过期:{result ?? "null"} {dt:yyyy-MM-dd HH:mm:ss} {token}";
XTrace.WriteLine(msg);
WriteHistory(node, "令牌过期", false, msg);
return null;
}
XTrace.WriteLine("借助令牌复活 {0}/{1}", code, token);
var dic = _session.CreateSession(token);
if (dic == null) XTrace.WriteLine($"借助令牌复活 CreateSession null!");
dic["Node"] = node;
WriteHistory(node, "令牌复活", true, $"[{code}]{token}");
return dic;
}
#endregion
#region
protected virtual void WriteHistory(INode node, String action, Boolean success, String remark)
{
NodeHistory.Create(node, action, success, remark, Environment.MachineName, UserHost);
}
#endregion
}
}

View File

@ -0,0 +1,512 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using NewLife.Log;
using NewLife.Remoting;
using NewLife.Security;
using NewLife.Serialization;
using Stardust.Data.Nodes;
using Stardust.Models;
using Stardust.Server.Common;
using XCode;
namespace Stardust.Server.Controllers
{
[ApiController]
[Route("[controller]")]
[ApiFilter]
public class NodeController : BaseController
{
#region
[HttpPost(nameof(Login))]
public LoginResponse Login(LoginInfo inf)
{
var code = inf.Code;
var secret = inf.Secret;
var node = Node.FindByCode(code, true);
var di = inf.Node;
// 校验唯一编码,防止客户端拷贝配置
if (node != null) node = CheckNode(node, di);
var autoReg = false;
if (node == null)
{
node = AutoRegister(null, inf, out autoReg);
}
else
{
if (!node.Enable) throw new ApiException(99, "禁止登录");
// 登录密码未设置或者未提交,则执行动态注册
if (node.Secret.IsNullOrEmpty() || secret.IsNullOrEmpty())
node = AutoRegister(node, inf, out autoReg);
else if (node.Secret.MD5() != secret)
node = AutoRegister(node, inf, out autoReg);
}
if (node == null) throw new ApiException(12, "节点鉴权失败");
var msg = "";
var success = false;
try
{
Fill(node, di);
//var ip = Request.Host + "";
var ip = UserHost;
node.Logins++;
node.LastLogin = DateTime.Now;
node.LastLoginIP = ip;
if (node.CreateIP.IsNullOrEmpty()) node.CreateIP = ip;
node.UpdateIP = ip;
node.Save();
// 设置令牌,可能已经进行用户登录
CreateToken(node.Code);
Session["Node"] = node;
// 在线记录
var olt = GetOnline(code, node);
if (olt == null) olt = CreateOnline(code, node);
olt.LocalTime = di.Time;
olt.MACs = di.Macs;
olt.COMs = di.COMs;
olt.Token = Token;
olt.PingCount++;
// 5秒内直接保存
if (olt.CreateTime.AddSeconds(5) > DateTime.Now)
olt.Save();
else
olt.SaveAsync();
msg = $"[{node.Name}/{node.Code}]鉴权成功 ";
success = true;
}
catch (Exception ex)
{
msg = ex.GetTrue().Message + " ";
throw;
}
finally
{
// 登录历史
WriteHistory("节点鉴权", success, node, msg + di.ToJson(false, false, false));
XTrace.WriteLine("登录{0} {1}", success ? "成功" : "失败", msg);
}
var rs = new LoginResponse
{
Name = node.Name,
Token = Token,
};
// 动态注册,下发节点证书
if (autoReg)
{
rs.Code = node.Code;
rs.Secret = node.Secret;
}
return rs;
}
/// <summary>注销</summary>
/// <param name="reason">注销原因</param>
/// <returns></returns>
[HttpGet(nameof(Logout))]
[HttpPost(nameof(Logout))]
public LoginResponse Logout(String reason)
{
var node = Session["Node"] as Node;
if (node != null)
{
var olt = GetOnline(node.Code, node);
if (olt != null)
{
var msg = "{3} [{0}]]登录于{1},最后活跃于{2}".F(node, olt.CreateTime, olt.UpdateTime, reason);
WriteHistory(node, "节点下线", true, msg);
olt.Delete();
// 计算在线时长
if (olt.CreateTime.Year > 2000)
{
node.OnlineTime += (Int32)(DateTime.Now - olt.CreateTime).TotalSeconds;
node.SaveAsync();
}
}
}
// 销毁会话,更新令牌
Session["Node"] = null;
CreateToken(null);
return new LoginResponse
{
Name = node?.Name,
Token = Token,
};
}
/// <summary>
/// 校验节点密钥
/// </summary>
/// <param name="node"></param>
/// <param name="ps"></param>
/// <returns></returns>
private Node CheckNode(Node node, NodeInfo di)
{
// 校验唯一编码,防止客户端拷贝配置
var uuid = di.UUID;
var guid = di.MachineGuid;
if (!uuid.IsNullOrEmpty() && uuid != node.Uuid)
{
WriteHistory("登录校验", false, node, "唯一标识不符!{0}!={1}".F(uuid, node.Uuid));
return null;
}
if (!guid.IsNullOrEmpty() && guid != node.MachineGuid)
{
WriteHistory("登录校验", false, node, "机器标识不符!{0}!={1}".F(guid, node.MachineGuid));
return null;
}
// 机器名
if (di.MachineName != node.MachineName)
{
WriteHistory("登录校验", false, node, "机器名不符!{0}!={1}".F(di.MachineName, node.MachineName));
}
// 网卡地址
if (di.Macs != node.MACs)
{
var dims = di.Macs?.Split(",") ?? new String[0];
var nodems = node.MACs?.Split(",") ?? new String[0];
// 任意网卡匹配则通过
if (!nodems.Any(e => dims.Contains(e)))
{
WriteHistory("登录校验", false, node, "网卡地址不符!{0}!={1}".F(di.Macs, node.MACs));
}
}
return node;
}
private Node AutoRegister(Node node, LoginInfo inf, out Boolean autoReg)
{
var set = Setting.Current;
if (!set.AutoRegister) throw new ApiException(12, "禁止自动注册");
var di = inf.Node;
if (node == null)
{
// 该硬件的所有节点信息
var list = Node.Search(di.UUID, di.MachineGuid, di.Macs);
// 当前节点信息,取较老者
list = list.OrderBy(e => e.ID).ToList();
// 找到节点
if (node == null) node = list.FirstOrDefault();
}
var ip = UserHost;
var name = "";
if (name.IsNullOrEmpty()) name = di.MachineName;
if (name.IsNullOrEmpty()) name = di.UserName;
if (node == null) node = new Node
{
Enable = true,
CreateIP = ip,
CreateTime = DateTime.Now,
};
// 如果未打开动态注册,则把节点修改为禁用
node.Enable = set.AutoRegister;
if (node.Name.IsNullOrEmpty()) node.Name = name;
// 优先使用节点散列来生成节点证书,确保节点路由到其它接入网关时保持相同证书代码
var code = "";
var uid = $"{di.UUID}@{di.MachineGuid}@{di.Macs}";
if (!uid.IsNullOrEmpty())
{
// 使用产品类别加密一下,确保不同类别有不同编码
var buf = uid.GetBytes();
code = buf.Crc().GetBytes().ToHex();
node.Code = code;
}
if (node.Code.IsNullOrEmpty()) code = Rand.NextString(8);
node.Secret = Rand.NextString(16);
node.UpdateIP = ip;
node.UpdateTime = DateTime.Now;
node.Save();
autoReg = true;
WriteHistory("动态注册", true, node, inf.ToJson(false, false, false));
return node;
}
private void Fill(Node node, NodeInfo di)
{
if (!di.OSName.IsNullOrEmpty()) node.OS = di.OSName;
if (!di.OSVersion.IsNullOrEmpty()) node.OSVersion = di.OSVersion;
if (!di.Version.IsNullOrEmpty()) node.Version = di.Version;
if (di.Compile.Year > 2000) node.CompileTime = di.Compile;
if (!di.MachineName.IsNullOrEmpty()) node.MachineName = di.MachineName;
if (!di.UserName.IsNullOrEmpty()) node.UserName = di.UserName;
if (!di.Processor.IsNullOrEmpty()) node.Processor = di.Processor;
if (!di.CpuID.IsNullOrEmpty()) node.CpuID = di.CpuID;
if (!di.UUID.IsNullOrEmpty()) node.Uuid = di.UUID;
if (!di.MachineGuid.IsNullOrEmpty()) node.MachineGuid = di.MachineGuid;
if (di.ProcessorCount > 0) node.Cpu = di.ProcessorCount;
if (di.Memory > 0) node.Memory = (Int32)(di.Memory / 1024 / 1024);
if (!di.Macs.IsNullOrEmpty()) node.MACs = di.Macs;
if (!di.COMs.IsNullOrEmpty()) node.COMs = di.COMs;
if (!di.InstallPath.IsNullOrEmpty()) node.InstallPath = di.InstallPath;
if (!di.Runtime.IsNullOrEmpty()) node.Runtime = di.Runtime;
}
#endregion
#region
[TokenFilter]
//[HttpGet(nameof(Ping))]
[HttpPost(nameof(Ping))]
public PingResponse Ping(PingInfo inf)
{
var rs = new PingResponse
{
Time = inf.Time,
ServerTime = DateTime.Now,
};
var node = Session["Node"] as Node;
if (node != null)
{
var code = node.Code;
var olt = GetOnline(code, node);
if (olt == null) olt = CreateOnline(code, node);
Fill(olt, inf);
olt.Token = Token;
olt.PingCount++;
olt.SaveAsync();
// 拉取命令
var cmds = NodeCommand.AcquireCommands(node.ID, 100);
if (cmds != null)
{
rs.Commands = cmds.Select(e => new CommandModel
{
Id = e.ID,
Command = e.Command,
Argument = e.Argument,
}).ToArray();
foreach (var item in cmds)
{
item.Finished = true;
item.UpdateTime = DateTime.Now;
}
cmds.Update(true);
}
}
return rs;
}
//[TokenFilter]
[HttpGet(nameof(Ping))]
public PingResponse Ping()
{
return new PingResponse
{
Time = 0,
ServerTime = DateTime.Now,
};
}
/// <summary>填充在线节点信息</summary>
/// <param name="olt"></param>
/// <param name="inf"></param>
private void Fill(NodeOnline olt, PingInfo inf)
{
if (inf.AvailableMemory > 0) olt.AvailableMemory = (Int32)(inf.AvailableMemory / 1024 / 1024);
if (inf.CpuRate > 0) olt.CpuRate = inf.CpuRate;
if (inf.Delay > 0) olt.Delay = inf.Delay;
var dt = inf.Time.ToDateTime();
if (dt.Year > 2000)
{
olt.LocalTime = dt;
olt.Offset = (Int32)Math.Round((dt - DateTime.Now).TotalSeconds);
}
if (!inf.Processes.IsNullOrEmpty()) olt.Processes = inf.Processes;
if (!inf.Macs.IsNullOrEmpty()) olt.MACs = inf.Macs;
if (!inf.COMs.IsNullOrEmpty()) olt.COMs = inf.COMs;
}
/// <summary></summary>
/// <param name="code"></param>
/// <param name="node"></param>
/// <returns></returns>
protected virtual NodeOnline GetOnline(String code, INode node)
{
var olt = Session["Online"] as NodeOnline;
if (olt != null) return olt;
var ip = UserHost;
var port = HttpContext.Connection.RemotePort;
var sid = $"{node.ID}@{ip}:{port}";
return NodeOnline.FindBySessionID(sid);
}
/// <summary>检查在线</summary>
/// <returns></returns>
protected virtual NodeOnline CreateOnline(String code, INode node)
{
var ip = UserHost;
var port = HttpContext.Connection.RemotePort;
var sid = $"{node.ID}@{ip}:{port}";
var olt = NodeOnline.GetOrAdd(sid);
olt.NodeID = node.ID;
olt.Name = node.Name;
olt.Version = node.Version;
olt.CompileTime = node.CompileTime;
olt.Memory = node.Memory;
olt.MACs = node.MACs;
olt.COMs = node.COMs;
olt.Token = Token;
olt.CreateIP = ip;
olt.Creator = Environment.MachineName;
Session["Online"] = olt;
return olt;
}
#endregion
#region
/// <summary>上报数据,针对命令</summary>
/// <param name="id"></param>
/// <returns></returns>
[HttpPost(nameof(Report))]
public async Task<Object> Report(Int32 id)
{
var node = Session["Node"] as Node;
if (node == null) throw new ApiException(402, "节点未登录");
var cmd = NodeCommand.FindByID(id);
if (cmd != null && cmd.NodeID == node.ID)
{
var ms = Request.Body;
if (Request.ContentLength > 0)
{
var rs = "";
switch (cmd.Command)
{
case "截屏":
rs = await SaveFileAsync(cmd, ms, "png");
break;
case "抓日志":
rs = await SaveFileAsync(cmd, ms, "log");
break;
default:
rs = await SaveFileAsync(cmd, ms, "bin");
break;
}
if (!rs.IsNullOrEmpty())
{
cmd.Result = rs;
cmd.Save();
WriteHistory(node, cmd.Command, true, rs);
}
}
}
return null;
}
private async Task<String> SaveFileAsync(NodeCommand cmd, Stream ms, String ext)
{
var file = $"../{cmd.Command}/{DateTime.Today:yyyyMMdd}/{cmd.NodeID}_{cmd.ID}.{ext}";
file.EnsureDirectory(true);
using var fs = file.AsFile().OpenWrite();
await ms.CopyToAsync(fs);
await ms.FlushAsync();
return file;
}
protected virtual NodeHistory WriteHistory(String action, Boolean success, INode node, String remark = null)
{
var ip = UserHost;
return NodeHistory.Create(node, action, success, remark, Environment.MachineName, ip);
}
#endregion
#region
/// <summary>升级检查</summary>
/// <param name="channel">更新通道</param>
/// <returns></returns>
[TokenFilter]
[HttpGet(nameof(Upgrade))]
public UpgradeInfo Upgrade(String channel)
{
var node = Session["Node"] as Node;
if (node == null) throw new ApiException(402, "节点未登录");
// 默认Release通道
if (!Enum.TryParse<NodeChannels>(channel, true, out var ch)) ch = NodeChannels.Release;
if (ch < NodeChannels.Release) ch = NodeChannels.Release;
// 找到所有产品版本
var list = NodeUpgrade.GetValids(ch);
// 应用过滤规则
var pv = list.FirstOrDefault(e => e.Match(node));
if (pv == null) return null;
//if (pv == null) throw new ApiException(509, "没有升级规则");
WriteHistory("自动更新", true, node, $"channel={ch} => [{pv.ID}] {pv.Version} {pv.Source} {pv.Executor}");
return new UpgradeInfo
{
Version = pv.Version,
Source = pv.Source,
Executor = pv.Executor,
Force = pv.Force,
Description = pv.Description,
};
}
#endregion
}
}

View File

@ -1,132 +1,90 @@
using NewLife;
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using NewLife;
using NewLife.Agent;
using NewLife.Log;
using NewLife.Net;
using NewLife.Remoting;
using NewLife.Threading;
using Stardust.Data;
using System;
namespace Stardust.Server
{
class Program
public class Program
{
static void Main(String[] args)
public static void Main(String[] args)
{
new MyService().Main();
if (Runtime.Windows)
{
var svc = new MyService
{
Args = args
};
svc.Main();
}
else
{
MyService.Run(args);
}
}
}
/// <summary>服务类。名字可以自定义</summary>
class MyService : AgentServiceBase<MyService>
{
/// <summary>是否使用线程池调度。false表示禁用线程池改用Agent线程</summary>
public Boolean Pooling { get; set; } = true;
public String[] Args { get; set; }
public MyService()
{
ServiceName = "Stardust";
ServiceName = "";
var set = XCode.Setting.Current;
if (set.IsNew)
{
set.Debug = true;
set.ShowSQL = false;
set.TraceSQLTime = 3000;
//set.SQLiteDbPath = @"..\Data";
set.Save();
}
var set2 = NewLife.Setting.Current;
if (set2.IsNew)
{
set2.DataPath = @"..\Data";
set2.Save();
}
ThreadPoolX.QueueUserWorkItem(() =>
{
var n = App.Meta.Count;
AppStat.Meta.Session.Dal.Db.ShowSQL = false;
});
// 注册菜单,在控制台菜单中按 t 可以执行Test函数主要用于临时处理数据
AddMenu('t', "数据测试", Test);
// 异步初始化
Task.Run(InitAsync);
}
ApiServer _Server;
private void Init()
{
var sc = _Server;
if (sc == null)
{
var set = Setting.Current;
sc = new ApiServer(set.Port)
{
Log = XTrace.Log
};
if (set.Debug)
{
var ns = sc.EnsureCreate() as NetServer;
ns.Log = XTrace.Log;
#if DEBUG
ns.LogSend = true;
ns.LogReceive = true;
sc.EncoderLog = XTrace.Log;
#endif
}
var local = new NetUri(NetType.Tcp, NetHelper.MyIP(), set.Port);
// 注册服务
sc.Register<StarService>();
var ds = new DiscoverService
{
Name = Environment.MachineName,
Local = local,
};
sc.Register(ds, null);
StarService.Log = XTrace.Log;
StarService.Local = local;
sc.Start();
_Server = sc;
}
}
/// <summary>服务启动</summary>
/// <remarks>
/// 安装Windows服务后服务启动会执行一次该方法。
/// 控制台菜单按5进入循环调试也会执行该方法。
/// </remarks>
private IWebHost _host;
protected override void StartWork(String reason)
{
Init();
_host = CreateWebHostBuilder(Args).Build();
_host.RunAsync();
base.StartWork(reason);
}
/// <summary>服务停止</summary>
/// <remarks>
/// 安装Windows服务后服务停止会执行该方法。
/// 控制台菜单按5进入循环调试任意键结束时也会执行该方法。
/// </remarks>
protected override void StopWork(String reason)
{
base.StopWork(reason);
_host.StopAsync().Wait(5_000);
_Server.TryDispose();
_Server = null;
base.StopWork(reason);
}
/// <summary>数据测试菜单t</summary>
public void Test()
public static void Run(String[] args)
{
XTrace.UseConsole();
// 异步初始化
Task.Run(InitAsync);
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(String[] args)
{
var set = Setting.Current;
var builder = WebHost.CreateDefaultBuilder(args);
if (set.Port > 0) builder.UseUrls($"http://*:{set.Port}");
builder.UseStartup<Startup>();
return builder;
}
private static void InitAsync()
{
// 初始化数据库
var n = App.Meta.Count;
AppStat.Meta.Session.Dal.Db.ShowSQL = false;
}
}
}

View File

@ -0,0 +1,27 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:2006/",
"sslPort": 44349
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Stardust.Server": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:5001;http://localhost:5000"
}
}
}

View File

@ -1,12 +1,12 @@
using System;
using System.ComponentModel;
using NewLife.Xml;
using NewLife.Configuration;
namespace Stardust.Server
{
/// <summary>配置</summary>
[XmlConfigFile("Config/Stardust.config", 15000)]
public class Setting : XmlConfig<Setting>
[Config("Stardust")]
public class Setting : Config<Setting>
{
#region
/// <summary>调试开关。默认true</summary>
@ -16,6 +16,14 @@ namespace Stardust.Server
/// <summary>服务端口。默认6666</summary>
[Description("服务端口。默认6666")]
public Int32 Port { get; set; } = 6666;
/// <summary>令牌有效期。默认2*3600秒</summary>
[Description("令牌有效期。默认2*3600秒")]
public Int32 TokenExpire { get; set; } = 2 * 3600;
/// <summary>自动注册。允许客户端自动注册默认true</summary>
[Description("自动注册。允许客户端自动注册默认true")]
public Boolean AutoRegister { get; set; } = true;
#endregion
#region

View File

@ -1,17 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net45;netcoreapp2.1</TargetFrameworks>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AssemblyTitle>星尘分布式服务</AssemblyTitle>
<Description>星尘,分布式服务框架。分布式资源调度,服务自动注册和发现,负载均衡,动态伸缩,故障转移,性能监控。</Description>
<Company>新生命开发团队</Company>
<Copyright>版权所有(C) 新生命开发团队 2019</Copyright>
<Version>1.0.2019.0526</Version>
<FileVersion>1.0.2019.0526</FileVersion>
<Copyright>©2002-2020 NewLife</Copyright>
<Version>1.0.2020.0312</Version>
<FileVersion>1.0.2020.0312</FileVersion>
<AssemblyVersion>1.0.*</AssemblyVersion>
<Deterministic>false</Deterministic>
<OutputPath>..\BinServer</OutputPath>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<DefineConstants>TRACE</DefineConstants>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)'=='netcoreapp2.1'">
@ -28,12 +31,6 @@
<Optimize>false</Optimize>
</PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)'=='net45'">
<Reference Include="System.ServiceProcess" />
<Reference Include="System.Web" />
<Reference Include="System.Web.Extensions" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="NewLife.Agent" Version="8.6.2020.204" />
<PackageReference Include="NewLife.XCode" Version="9.16.2020.308" />
@ -44,12 +41,6 @@
<ProjectReference Include="..\Stardust\Stardust.csproj" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp2.1'">
<PackageReference Include="NewLife.Agent">
<Version>8.1.2019.618</Version>
</PackageReference>
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="del &quot;$(TargetDir)*.xml&quot; /q" />
</Target>

View File

@ -0,0 +1,49 @@
using System.Text.Encodings.Web;
using System.Text.Unicode;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Stardust.Server.Common;
namespace Stardust.Server
{
public class Startup
{
public Startup(IConfiguration configuration) => Configuration = configuration;
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpClient();
services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.Converters.Add(new DateTimeConverter());
options.JsonSerializerOptions.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All);
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
//app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
}

View File

@ -0,0 +1,37 @@
using System;
namespace Stardust.Models
{
/// <summary>节点登录信息</summary>
public class LoginInfo
{
#region
/// <summary>节点编码</summary>
public String Code { get; set; }
/// <summary>节点密钥</summary>
public String Secret { get; set; }
/// <summary>节点信息</summary>
public NodeInfo Node { get; set; }
#endregion
}
/// <summary>节点登录响应</summary>
public class LoginResponse
{
#region
/// <summary>节点编码</summary>
public String Code { get; set; }
/// <summary>节点密钥</summary>
public String Secret { get; set; }
/// <summary>名称</summary>
public String Name { get; set; }
/// <summary>令牌</summary>
public String Token { get; set; }
#endregion
}
}

View File

@ -0,0 +1,67 @@
using System;
namespace Stardust.Models
{
/// <summary>节点信息</summary>
public class NodeInfo
{
#region
/// <summary>版本</summary>
public String Version { get; set; }
/// <summary>编译时间</summary>
public DateTime Compile { get; set; }
/// <summary>系统名</summary>
public String OSName { get; set; }
/// <summary>系统版本</summary>
public String OSVersion { get; set; }
/// <summary>机器名</summary>
public String MachineName { get; set; }
/// <summary>用户名</summary>
public String UserName { get; set; }
/// <summary>核心数</summary>
public Int32 ProcessorCount { get; set; }
/// <summary>内存大小</summary>
public UInt64 Memory { get; set; }
/// <summary>可用内存大小</summary>
public UInt64 AvailableMemory { get; set; }
/// <summary>处理器</summary>
public String Processor { get; set; }
/// <summary>处理器标识</summary>
public String CpuID { get; set; }
/// <summary>主频</summary>
public Single CpuRate { get; set; }
/// <summary>唯一标识</summary>
public String UUID { get; set; }
/// <summary>机器标识</summary>
public String MachineGuid { get; set; }
/// <summary>MAC地址</summary>
public String Macs { get; set; }
/// <summary>串口</summary>
public String COMs { get; set; }
/// <summary>安装路径</summary>
public String InstallPath { get; set; }
/// <summary>运行时</summary>
public String Runtime { get; set; }
/// <summary>本地时间</summary>
public DateTime Time { get; set; }
#endregion
}
}

View File

@ -0,0 +1,59 @@
using System;
namespace Stardust.Models
{
/// <summary>心跳信息</summary>
public class PingInfo
{
#region
/// <summary>可用内存大小</summary>
public UInt64 AvailableMemory { get; set; }
/// <summary>主频</summary>
public Single CpuRate { get; set; }
/// <summary>MAC地址</summary>
public String Macs { get; set; }
/// <summary>串口</summary>
public String COMs { get; set; }
/// <summary>进程列表</summary>
public String Processes { get; set; }
/// <summary>本地时间。ms毫秒</summary>
public Int64 Time { get; set; }
/// <summary>延迟</summary>
public Int32 Delay { get; set; }
#endregion
}
/// <summary>心跳响应</summary>
public class PingResponse
{
/// <summary>本地时间。ms毫秒</summary>
public Int64 Time { get; set; }
/// <summary>服务器时间</summary>
public DateTime ServerTime { get; set; }
/// <summary>下发命令</summary>
public CommandModel[] Commands { get; set; }
}
/// <summary>命令模型</summary>
public class CommandModel
{
/// <summary>序号</summary>
public Int32 Id { get; set; }
/// <summary>命令</summary>
public String Command { get; set; }
/// <summary>参数</summary>
public String Argument { get; set; }
//public String Result { get; set; }
}
}

View File

@ -0,0 +1,23 @@
using System;
namespace Stardust.Models
{
/// <summary>更新响应</summary>
public class UpgradeInfo
{
/// <summary>版本号</summary>
public String Version { get; set; }
/// <summary>更新源Url地址</summary>
public String Source { get; set; }
/// <summary>更新后要执行的命令</summary>
public String Executor { get; set; }
/// <summary>是否强制更新,不需要用户同意</summary>
public Boolean Force { get; set; }
/// <summary>描述</summary>
public String Description { get; set; }
}
}

View File

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AssemblyVersion>1.0.*</AssemblyVersion>
<Deterministic>false</Deterministic>
</PropertyGroup>

View File

@ -5,7 +5,7 @@ VisualStudioVersion = 16.0.28922.388
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stardust", "Stardust\Stardust.csproj", "{AADBD913-749C-467E-A63F-C118C4C82351}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stardust.Web", "Stardust.Web\Stardust.Web.csproj", "{25331DEF-FEE3-44D5-A4E9-864078441F71}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stardust.Web", "Stardust.Web\Stardust.Web.csproj", "{25331DEF-FEE3-44D5-A4E9-864078441F71}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Others", "Others", "{0EA980BB-BB15-41A3-B75B-537BC42E985B}"
ProjectSection(SolutionItems) = preProject
@ -22,6 +22,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stardust.Server", "Stardust
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StarAgent", "StarAgent\StarAgent.csproj", "{0FF65D90-214F-405E-9674-6C0992BC61FD}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8D122272-87BB-4968-8A2F-CFF04CE47B29}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU