加固 Config/Signal 路径解析,前端 DataCenter 表单类型与 API 对齐。 Co-authored-by: Cursor <cursoragent@cursor.com>
290 lines
9.9 KiB
C#
290 lines
9.9 KiB
C#
using System.ComponentModel;
|
|
using System.Reflection;
|
|
using System.Text.Json.Serialization;
|
|
|
|
namespace MiGu.Server.Signal;
|
|
|
|
/// <summary>
|
|
/// 从 Simple3 <c>plugins/StandardScene.Signal.dll</c> 反射数据中心表和列。
|
|
/// 平台不编译引用该插件工程,避免仓外路径和 Windows TFM 耦合。
|
|
/// </summary>
|
|
public static class SignalModelSchemaResolver
|
|
{
|
|
private const string ModelNamespace = "StandardScene.Signal.Model";
|
|
private const string AssemblyFileName = "StandardScene.Signal.dll";
|
|
|
|
public static string? FindAssemblyPath(string? pluginsDir)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(pluginsDir))
|
|
return null;
|
|
var path = Path.Combine(pluginsDir, AssemblyFileName);
|
|
return File.Exists(path) ? path : null;
|
|
}
|
|
|
|
public static IReadOnlyList<SignalTableDef> ResolveTables(string? pluginsDir, string? workingDirectory = null)
|
|
{
|
|
return WithSignalAssembly(pluginsDir, workingDirectory, assembly =>
|
|
{
|
|
if (assembly == null)
|
|
return Array.Empty<SignalTableDef>();
|
|
try
|
|
{
|
|
return BuildTablesFromAssembly(assembly);
|
|
}
|
|
catch
|
|
{
|
|
return Array.Empty<SignalTableDef>();
|
|
}
|
|
});
|
|
}
|
|
|
|
public static IReadOnlyList<SignalColumn> ResolveColumns(string? modelName, string? pluginsDir, string? workingDirectory = null)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(modelName))
|
|
return Array.Empty<SignalColumn>();
|
|
|
|
return WithSignalAssembly(pluginsDir, workingDirectory, assembly =>
|
|
{
|
|
if (assembly == null)
|
|
return Array.Empty<SignalColumn>();
|
|
try
|
|
{
|
|
var type = assembly.GetType($"{ModelNamespace}.{modelName.Trim()}", throwOnError: false, ignoreCase: true);
|
|
if (type == null)
|
|
return Array.Empty<SignalColumn>();
|
|
return DiscoverColumns(type);
|
|
}
|
|
catch
|
|
{
|
|
return Array.Empty<SignalColumn>();
|
|
}
|
|
});
|
|
}
|
|
|
|
private static T WithSignalAssembly<T>(string? pluginsDir, string? workingDirectory, Func<Assembly?, T> use)
|
|
{
|
|
var assemblyPath = FindAssemblyPath(pluginsDir);
|
|
if (assemblyPath == null)
|
|
return use(null);
|
|
|
|
var expected = Path.GetFullPath(assemblyPath);
|
|
var cached = FindLoadedFrom(expected);
|
|
if (cached != null)
|
|
return use(cached);
|
|
|
|
var probeDirs = BuildProbeDirs(pluginsDir, workingDirectory);
|
|
ResolveEventHandler handler = (_, args) => ResolveAssembly(args.Name, probeDirs);
|
|
AppDomain.CurrentDomain.AssemblyResolve += handler;
|
|
try
|
|
{
|
|
var loaded = Assembly.LoadFrom(expected);
|
|
return use(HasModelTypes(loaded) ? loaded : null);
|
|
}
|
|
catch
|
|
{
|
|
return use(null);
|
|
}
|
|
finally
|
|
{
|
|
AppDomain.CurrentDomain.AssemblyResolve -= handler;
|
|
}
|
|
}
|
|
|
|
private static Assembly? FindLoadedFrom(string expectedFullPath)
|
|
{
|
|
try
|
|
{
|
|
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
|
{
|
|
if (!string.Equals(assembly.GetName().Name, "StandardScene.Signal", StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
if (string.IsNullOrWhiteSpace(assembly.Location))
|
|
continue;
|
|
if (!string.Equals(Path.GetFullPath(assembly.Location), expectedFullPath, StringComparison.OrdinalIgnoreCase))
|
|
continue;
|
|
return HasModelTypes(assembly) ? assembly : null;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
/* 继续 LoadFrom */
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static bool HasModelTypes(Assembly assembly)
|
|
{
|
|
return SafeGetTypes(assembly).Any(t =>
|
|
t != null && string.Equals(t.Namespace, ModelNamespace, StringComparison.Ordinal));
|
|
}
|
|
|
|
private static IReadOnlyList<string> BuildProbeDirs(string? pluginsDir, string? workingDirectory)
|
|
{
|
|
var dirs = new List<string>();
|
|
void Add(string? dir)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(dir)) return;
|
|
var full = Path.GetFullPath(dir);
|
|
if (Directory.Exists(full) && !dirs.Contains(full, StringComparer.OrdinalIgnoreCase))
|
|
dirs.Add(full);
|
|
}
|
|
|
|
Add(pluginsDir);
|
|
Add(workingDirectory);
|
|
if (!string.IsNullOrWhiteSpace(pluginsDir))
|
|
Add(Path.GetDirectoryName(pluginsDir));
|
|
|
|
return dirs;
|
|
}
|
|
|
|
private static Assembly? ResolveAssembly(string? assemblyName, IReadOnlyList<string> probeDirs)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(assemblyName))
|
|
return null;
|
|
|
|
string simpleName;
|
|
try
|
|
{
|
|
simpleName = new AssemblyName(assemblyName).Name ?? assemblyName;
|
|
}
|
|
catch
|
|
{
|
|
simpleName = assemblyName.Split(',')[0];
|
|
}
|
|
|
|
foreach (var dir in probeDirs)
|
|
{
|
|
var path = Path.Combine(dir, simpleName + ".dll");
|
|
if (!File.Exists(path))
|
|
continue;
|
|
try
|
|
{
|
|
return Assembly.LoadFrom(path);
|
|
}
|
|
catch
|
|
{
|
|
/* try next dir */
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static IReadOnlyList<SignalTableDef> BuildTablesFromAssembly(Assembly assembly)
|
|
{
|
|
var found = new List<(int Order, SignalTableDef Table)>();
|
|
foreach (var type in SafeGetTypes(assembly))
|
|
{
|
|
if (type == null || !string.Equals(type.Namespace, ModelNamespace, StringComparison.Ordinal))
|
|
continue;
|
|
|
|
var attr = type.GetCustomAttributes(inherit: false)
|
|
.FirstOrDefault(a => a.GetType().Name == "SignalTableAttribute");
|
|
if (attr == null)
|
|
continue;
|
|
|
|
var attrType = attr.GetType();
|
|
var id = (attrType.GetProperty("Id")?.GetValue(attr) as string ?? "").Trim();
|
|
var fileName = (attrType.GetProperty("FileName")?.GetValue(attr) as string ?? "").Trim();
|
|
var title = (attrType.GetProperty("Title")?.GetValue(attr) as string ?? "").Trim();
|
|
var order = attrType.GetProperty("Order")?.GetValue(attr) as int? ?? 0;
|
|
if (string.IsNullOrWhiteSpace(id) || string.IsNullOrWhiteSpace(fileName))
|
|
continue;
|
|
|
|
var category = type.GetCustomAttribute<CategoryAttribute>()?.Category ?? "";
|
|
if (string.IsNullOrWhiteSpace(title))
|
|
title = type.GetCustomAttribute<DisplayNameAttribute>()?.DisplayName ?? id;
|
|
|
|
found.Add((order, new SignalTableDef(id, title, category, fileName, DiscoverColumns(type))));
|
|
}
|
|
|
|
return found.OrderBy(x => x.Order).ThenBy(x => x.Table.Id).Select(x => x.Table).ToList();
|
|
}
|
|
|
|
private static IEnumerable<Type> SafeGetTypes(Assembly assembly)
|
|
{
|
|
try
|
|
{
|
|
return assembly.GetTypes();
|
|
}
|
|
catch (ReflectionTypeLoadException ex)
|
|
{
|
|
return ex.Types.Where(t => t != null)!;
|
|
}
|
|
}
|
|
|
|
private static IReadOnlyList<SignalColumn> DiscoverColumns(Type type)
|
|
{
|
|
var list = new List<SignalColumn>();
|
|
foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
|
.Where(p => p.CanRead && p.CanWrite && p.GetIndexParameters().Length == 0)
|
|
.Where(p => p.GetCustomAttribute<BrowsableAttribute>()?.Browsable != false)
|
|
.Where(p => !IsJsonIgnored(p))
|
|
.OrderBy(p => p.MetadataToken))
|
|
{
|
|
var label = prop.GetCustomAttribute<DisplayNameAttribute>()?.DisplayName;
|
|
if (string.IsNullOrWhiteSpace(label))
|
|
label = prop.Name;
|
|
|
|
var columnType = MapType(prop.PropertyType);
|
|
string[]? options = null;
|
|
var group = prop.GetCustomAttribute<CategoryAttribute>(inherit: false)?.Category;
|
|
if (string.IsNullOrWhiteSpace(group))
|
|
group = null;
|
|
|
|
var select = ReadSelectOptions(prop);
|
|
if (select is { Length: > 0 })
|
|
{
|
|
columnType = "enum";
|
|
options = select;
|
|
}
|
|
else if (columnType == "enum" && (Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType).IsEnum)
|
|
{
|
|
options = Enum.GetNames(Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType);
|
|
}
|
|
|
|
list.Add(new SignalColumn(prop.Name, label, columnType, options, group));
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
private static bool IsJsonIgnored(PropertyInfo prop)
|
|
{
|
|
if (prop.GetCustomAttribute<JsonIgnoreAttribute>() != null)
|
|
return true;
|
|
|
|
foreach (var attr in prop.GetCustomAttributes(inherit: true))
|
|
{
|
|
if (attr.GetType().FullName == "Newtonsoft.Json.JsonIgnoreAttribute")
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static string[]? ReadSelectOptions(PropertyInfo prop)
|
|
{
|
|
var attr = prop.GetCustomAttributes(inherit: false)
|
|
.FirstOrDefault(a => a.GetType().Name == "SignalSelectAttribute");
|
|
if (attr == null)
|
|
return null;
|
|
var options = attr.GetType().GetProperty("Options")?.GetValue(attr) as string[];
|
|
return options is { Length: > 0 } ? options : null;
|
|
}
|
|
|
|
private static string MapType(Type type)
|
|
{
|
|
var underlying = Nullable.GetUnderlyingType(type) ?? type;
|
|
if (underlying == typeof(bool))
|
|
return "bool";
|
|
if (underlying == typeof(int) || underlying == typeof(long) || underlying == typeof(short) ||
|
|
underlying == typeof(byte) || underlying == typeof(uint) || underlying == typeof(ulong))
|
|
return "int";
|
|
if (underlying.IsEnum)
|
|
return "enum";
|
|
return "string";
|
|
}
|
|
}
|