Signal 数据中心改为运行时加载 schema,解除 StandardScene 工程编译耦合。
加固 Config/Signal 路径解析,前端 DataCenter 表单类型与 API 对齐。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -15,8 +15,8 @@ public sealed record SignalTableDef(
|
|||||||
IReadOnlyList<SignalColumn> Columns);
|
IReadOnlyList<SignalColumn> Columns);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 读写 SimpleLite 工作目录 <c>Config/Signal/*.json</c>,供迷毂「数据中心」表格编辑。
|
/// 读写 Simple3 工作目录 <c>Config/Signal/*.json</c>,供迷毂「数据中心」表格编辑。
|
||||||
/// 表结构优先从 StandardScene.Signal.dll 反射;无插件时才读 signal-tables.json。
|
/// 表结构只从 Simple3 <c>plugins/StandardScene.Signal.dll</c> 反射;无插件时才读 signal-tables.json。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class SignalDataStore
|
public sealed class SignalDataStore
|
||||||
{
|
{
|
||||||
@@ -28,11 +28,11 @@ public sealed class SignalDataStore
|
|||||||
|
|
||||||
private static readonly object FileLock = new();
|
private static readonly object FileLock = new();
|
||||||
|
|
||||||
private readonly SimpleLiteLauncher _launcher;
|
private readonly Simple3Launcher _launcher;
|
||||||
private IReadOnlyList<SignalTableDef>? _cachedTables;
|
private IReadOnlyList<SignalTableDef>? _cachedTables;
|
||||||
private long _cachedSignature;
|
private long _cachedSignature;
|
||||||
|
|
||||||
public SignalDataStore(SimpleLiteLauncher launcher) => _launcher = launcher;
|
public SignalDataStore(Simple3Launcher launcher) => _launcher = launcher;
|
||||||
|
|
||||||
public IReadOnlyList<SignalTableDef> GetTables()
|
public IReadOnlyList<SignalTableDef> GetTables()
|
||||||
{
|
{
|
||||||
@@ -55,9 +55,11 @@ public sealed class SignalDataStore
|
|||||||
{
|
{
|
||||||
var wd = _launcher.ResolveWorkingDirectory();
|
var wd = _launcher.ResolveWorkingDirectory();
|
||||||
if (string.IsNullOrWhiteSpace(wd))
|
if (string.IsNullOrWhiteSpace(wd))
|
||||||
return (null, "未找到 SimpleLite 工作目录,无法定位 Config/Signal");
|
return (null, "未找到 Simple3 工作目录,无法定位 Config/Signal");
|
||||||
|
|
||||||
|
if (!TryResolveUnderSignalDir(wd, table.FileName, out var dest, out var pathError))
|
||||||
|
return (null, pathError);
|
||||||
|
|
||||||
var dest = Path.GetFullPath(Path.Combine(wd, "Config", "Signal", table.FileName));
|
|
||||||
if (File.Exists(dest))
|
if (File.Exists(dest))
|
||||||
return (dest, null);
|
return (dest, null);
|
||||||
|
|
||||||
@@ -109,4 +111,29 @@ public sealed class SignalDataStore
|
|||||||
lock (FileLock)
|
lock (FileLock)
|
||||||
File.WriteAllText(path, rows.ToJsonString(FileJson));
|
File.WriteAllText(path, rows.ToJsonString(FileJson));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryResolveUnderSignalDir(string workingDirectory, string fileName, out string dest, out string error)
|
||||||
|
{
|
||||||
|
dest = "";
|
||||||
|
error = "";
|
||||||
|
if (string.IsNullOrWhiteSpace(fileName) ||
|
||||||
|
Path.IsPathRooted(fileName) ||
|
||||||
|
fileName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0)
|
||||||
|
{
|
||||||
|
error = "信号表文件名非法";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var root = Path.GetFullPath(Path.Combine(workingDirectory, "Config", "Signal"));
|
||||||
|
dest = Path.GetFullPath(Path.Combine(root, fileName));
|
||||||
|
var prefix = root.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||||
|
if (!dest.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
error = "信号表路径超出 Config/Signal";
|
||||||
|
dest = "";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using StandardScene.Signal.Model;
|
|
||||||
|
|
||||||
namespace MiGu.Server.Signal;
|
namespace MiGu.Server.Signal;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 从 StandardScene.Signal 程序集反射数据中心表和列。
|
/// 从 Simple3 <c>plugins/StandardScene.Signal.dll</c> 反射数据中心表和列。
|
||||||
/// 优先使用 MiGu.Server 编译期引用的程序集;否则再从 SimpleLite plugins 加载。
|
/// 平台不编译引用该插件工程,避免仓外路径和 Windows TFM 耦合。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class SignalModelSchemaResolver
|
public static class SignalModelSchemaResolver
|
||||||
{
|
{
|
||||||
@@ -24,18 +23,19 @@ public static class SignalModelSchemaResolver
|
|||||||
|
|
||||||
public static IReadOnlyList<SignalTableDef> ResolveTables(string? pluginsDir, string? workingDirectory = null)
|
public static IReadOnlyList<SignalTableDef> ResolveTables(string? pluginsDir, string? workingDirectory = null)
|
||||||
{
|
{
|
||||||
var assembly = TryGetSignalAssembly(pluginsDir, workingDirectory);
|
return WithSignalAssembly(pluginsDir, workingDirectory, assembly =>
|
||||||
if (assembly == null)
|
|
||||||
return Array.Empty<SignalTableDef>();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
return BuildTablesFromAssembly(assembly);
|
if (assembly == null)
|
||||||
}
|
return Array.Empty<SignalTableDef>();
|
||||||
catch
|
try
|
||||||
{
|
{
|
||||||
return Array.Empty<SignalTableDef>();
|
return BuildTablesFromAssembly(assembly);
|
||||||
}
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return Array.Empty<SignalTableDef>();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IReadOnlyList<SignalColumn> ResolveColumns(string? modelName, string? pluginsDir, string? workingDirectory = null)
|
public static IReadOnlyList<SignalColumn> ResolveColumns(string? modelName, string? pluginsDir, string? workingDirectory = null)
|
||||||
@@ -43,61 +43,76 @@ public static class SignalModelSchemaResolver
|
|||||||
if (string.IsNullOrWhiteSpace(modelName))
|
if (string.IsNullOrWhiteSpace(modelName))
|
||||||
return Array.Empty<SignalColumn>();
|
return Array.Empty<SignalColumn>();
|
||||||
|
|
||||||
var assembly = TryGetSignalAssembly(pluginsDir, workingDirectory);
|
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 (assembly == null)
|
||||||
if (type == null)
|
|
||||||
return Array.Empty<SignalColumn>();
|
return Array.Empty<SignalColumn>();
|
||||||
|
try
|
||||||
return DiscoverColumns(type);
|
{
|
||||||
}
|
var type = assembly.GetType($"{ModelNamespace}.{modelName.Trim()}", throwOnError: false, ignoreCase: true);
|
||||||
catch
|
if (type == null)
|
||||||
{
|
return Array.Empty<SignalColumn>();
|
||||||
return Array.Empty<SignalColumn>();
|
return DiscoverColumns(type);
|
||||||
}
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return Array.Empty<SignalColumn>();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Assembly? TryGetSignalAssembly(string? pluginsDir, string? workingDirectory)
|
private static T WithSignalAssembly<T>(string? pluginsDir, string? workingDirectory, Func<Assembly?, T> use)
|
||||||
{
|
{
|
||||||
try
|
|
||||||
{
|
|
||||||
var referenced = typeof(PlcStationModel).Assembly;
|
|
||||||
if (HasModelTypes(referenced))
|
|
||||||
return referenced;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
/* 未引用插件工程时继续走 LoadFrom */
|
|
||||||
}
|
|
||||||
|
|
||||||
var assemblyPath = FindAssemblyPath(pluginsDir);
|
var assemblyPath = FindAssemblyPath(pluginsDir);
|
||||||
if (assemblyPath == null)
|
if (assemblyPath == null)
|
||||||
return null;
|
return use(null);
|
||||||
|
|
||||||
|
var expected = Path.GetFullPath(assemblyPath);
|
||||||
|
var cached = FindLoadedFrom(expected);
|
||||||
|
if (cached != null)
|
||||||
|
return use(cached);
|
||||||
|
|
||||||
var probeDirs = BuildProbeDirs(pluginsDir, workingDirectory);
|
var probeDirs = BuildProbeDirs(pluginsDir, workingDirectory);
|
||||||
ResolveEventHandler? handler = null;
|
ResolveEventHandler handler = (_, args) => ResolveAssembly(args.Name, probeDirs);
|
||||||
handler = (_, args) => ResolveAssembly(args.Name, probeDirs);
|
|
||||||
AppDomain.CurrentDomain.AssemblyResolve += handler;
|
AppDomain.CurrentDomain.AssemblyResolve += handler;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var loaded = Assembly.LoadFrom(assemblyPath);
|
var loaded = Assembly.LoadFrom(expected);
|
||||||
return HasModelTypes(loaded) ? loaded : null;
|
return use(HasModelTypes(loaded) ? loaded : null);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
return null;
|
return use(null);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
if (handler != null)
|
AppDomain.CurrentDomain.AssemblyResolve -= handler;
|
||||||
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)
|
private static bool HasModelTypes(Assembly assembly)
|
||||||
{
|
{
|
||||||
return SafeGetTypes(assembly).Any(t =>
|
return SafeGetTypes(assembly).Any(t =>
|
||||||
@@ -119,7 +134,6 @@ public static class SignalModelSchemaResolver
|
|||||||
Add(workingDirectory);
|
Add(workingDirectory);
|
||||||
if (!string.IsNullOrWhiteSpace(pluginsDir))
|
if (!string.IsNullOrWhiteSpace(pluginsDir))
|
||||||
Add(Path.GetDirectoryName(pluginsDir));
|
Add(Path.GetDirectoryName(pluginsDir));
|
||||||
Add(AppContext.BaseDirectory);
|
|
||||||
|
|
||||||
return dirs;
|
return dirs;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ public static class SignalTableManifestLoader
|
|||||||
PropertyNameCaseInsensitive = true
|
PropertyNameCaseInsensitive = true
|
||||||
};
|
};
|
||||||
|
|
||||||
public static IReadOnlyList<SignalTableDef> Load(SimpleLiteLauncher launcher)
|
public static IReadOnlyList<SignalTableDef> Load(Simple3Launcher launcher)
|
||||||
{
|
{
|
||||||
var workingDirectory = launcher.ResolveWorkingDirectory();
|
var workingDirectory = launcher.ResolveWorkingDirectory();
|
||||||
var pluginsDir = launcher.ResolvePluginsDir();
|
var pluginsDir = launcher.ResolvePluginsDir();
|
||||||
@@ -31,7 +31,7 @@ public static class SignalTableManifestLoader
|
|||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static long ComputeManifestSignature(SimpleLiteLauncher launcher)
|
public static long ComputeManifestSignature(Simple3Launcher launcher)
|
||||||
{
|
{
|
||||||
long sig = 0;
|
long sig = 0;
|
||||||
foreach (var path in ResolveManifestPaths(launcher))
|
foreach (var path in ResolveManifestPaths(launcher))
|
||||||
@@ -61,7 +61,7 @@ public static class SignalTableManifestLoader
|
|||||||
return sig;
|
return sig;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SignalTableManifest TryLoadManifest(SimpleLiteLauncher launcher)
|
private static SignalTableManifest TryLoadManifest(Simple3Launcher launcher)
|
||||||
{
|
{
|
||||||
foreach (var path in ResolveManifestPaths(launcher))
|
foreach (var path in ResolveManifestPaths(launcher))
|
||||||
{
|
{
|
||||||
@@ -80,7 +80,7 @@ public static class SignalTableManifestLoader
|
|||||||
?? new SignalTableManifest();
|
?? new SignalTableManifest();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IEnumerable<string> ResolveManifestPaths(SimpleLiteLauncher launcher)
|
private static IEnumerable<string> ResolveManifestPaths(Simple3Launcher launcher)
|
||||||
{
|
{
|
||||||
var wd = launcher.ResolveWorkingDirectory();
|
var wd = launcher.ResolveWorkingDirectory();
|
||||||
if (!string.IsNullOrWhiteSpace(wd))
|
if (!string.IsNullOrWhiteSpace(wd))
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export async function listSignalTables(): Promise<SignalDataListDto> {
|
|||||||
if (MOCK) {
|
if (MOCK) {
|
||||||
return {
|
return {
|
||||||
signalEnabled: true,
|
signalEnabled: true,
|
||||||
workingDirectory: 'D:\\工作\\stand\\SimpleLite',
|
workingDirectory: 'D:\\工作\\stand\\Simple3',
|
||||||
tables: [
|
tables: [
|
||||||
{ id: 'stations', title: 'PLC机构', category: 'PLC数据管理', fileName: 'stations.json' },
|
{ id: 'stations', title: 'PLC机构', category: 'PLC数据管理', fileName: 'stations.json' },
|
||||||
{ id: 'docks', title: '机构工位', category: 'PLC数据管理', fileName: 'station-docks.json' },
|
{ id: 'docks', title: '机构工位', category: 'PLC数据管理', fileName: 'station-docks.json' },
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<h2>{{ table?.title ?? title }}</h2>
|
<h2>{{ table?.title ?? title }}</h2>
|
||||||
<p>
|
<p>
|
||||||
读写 SimpleLite 工作目录 <code>Config/Signal</code>;改完后到信号交互进程点重新加载配置。
|
读写 Simple3 工作目录 <code>Config/Signal</code>;改完后到信号交互进程点重新加载配置。
|
||||||
点位为 BOOL 时按「字节 + 位」直接读写。
|
点位为 BOOL 时按「字节 + 位」直接读写。
|
||||||
<span v-if="table">
|
<span v-if="table">
|
||||||
({{ table.fileName }}{{ table.exists ? '' : ' · 文件尚未创建,保存后写入' }})
|
({{ table.fileName }}{{ table.exists ? '' : ' · 文件尚未创建,保存后写入' }})
|
||||||
@@ -117,7 +117,7 @@ const formGroups = computed(() => {
|
|||||||
|
|
||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
const editingIndex = ref(-1)
|
const editingIndex = ref(-1)
|
||||||
const form = reactive<Record<string, unknown>>({})
|
const form = reactive<Record<string, any>>({})
|
||||||
|
|
||||||
function truthy(v: unknown): boolean {
|
function truthy(v: unknown): boolean {
|
||||||
return v === true || v === 'true' || v === 1 || v === '1'
|
return v === true || v === 'true' || v === 1 || v === '1'
|
||||||
|
|||||||
Reference in New Issue
Block a user