fix(node): 统一 adapter 合同加载入口
This commit is contained in:
parent
72f7bd9107
commit
67ee7c244e
|
|
@ -8,6 +8,8 @@
|
|||
|
||||
## Unreleased
|
||||
|
||||
- Windows Node 的每个专业软件 adapter 现在自带唯一运行时合同,不再受安装目录遗留共享合同影响;诊断信息会显示实际合同路径、摘要和 Workspace 协议状态,服务端也会阻止协议不匹配的任务反复派发。
|
||||
|
||||
- 修复专业软件持久工程首次建单失败后反复重试的问题;新任务会先建立工程空间再提交执行,并在数据库完整性异常时返回可诊断的错误。
|
||||
|
||||
- 专业软件任务失败或取消后会在原对话留下持久通知,页面离线、刷新或对话暂时忙碌时也不会错过结果。
|
||||
|
|
|
|||
|
|
@ -462,7 +462,7 @@ scheduled_jobs(§8.5) channel_bindings(§8.7,判别列+JSONB)
|
|||
|
||||
云端控制面使用独立的 `software_node_enrollments` 与 `software_nodes`,不复用用户外部系统连接。管理员创建的一次性注册码具有 128 bit 随机熵,数据库只保存 SHA-256 摘要;节点注册在行锁事务中校验有效期、预期名称和服务端支持的能力,成功后原子消费。注册码只建立节点身份,不要求管理员逐项授权专业软件能力。每个节点获得独立高熵 Token,数据库只保存 bcrypt 强哈希,明文仅在注册响应出现一次。
|
||||
|
||||
专业软件采用“共享能力契约 + 本机 adapter”边界。仓库根目录 `software-contracts/*.json` 是语言无关的声明式事实源,描述 capability、请求 JSON Schema、输入额度、输出 manifest、feature 与最低 adapter 版本;Core 按文件签名热加载契约,只负责身份、账本、调度、传输、摘要与最终发布,不包含 Origin 或其他软件的操作分支。热加载先完整校验新快照再原子切换,写入中或非法版本继续使用上一份有效快照。Windows Node 是稳定的可信宿主,只负责持久化 job 目录、下载/上传、恢复、取消,以及从程序目录 `adapters/*/adapter.json` 发现 adapter;Host 对请求只做通用 JSON Schema 校验,具体软件探测和二次语义校验属于 Worker。manifest 声明 capability、实际 adapter 版本、运行类型、入口和契约文件,并可声明 1–1440 分钟的单任务超时;旧 manifest 缺省时保持 30 分钟,版本与时限不编译进 EXE。完整 Node 包按目录自动收集 adapter,并把根目录契约复制到只读 `software-contracts/`;单 adapter 更新包仍可把同一契约副本放在 adapter 目录,加载时本地副本优先,两者都不形成源码第二事实源。Worker 可以是受管 Python 脚本或独立 EXE,进程只接收 job 目录,并以 `--probe` 返回运行状态,通过 `state.json`、`terminal.json` 与固定输出目录交接;Python、COM 或某个 SDK 都不属于通用协议。当前 Origin 和 ANSYS adapter 使用 Python 只是实现选择。
|
||||
专业软件采用“共享能力契约 + 本机 adapter”边界。仓库根目录 `software-contracts/*.json` 是语言无关的声明式源码事实源,描述 capability、请求 JSON Schema、输入额度、输出 manifest、feature 与最低 adapter 版本;Core 按文件签名热加载契约,只负责身份、账本、调度、传输、摘要与最终发布,不包含 Origin 或其他软件的操作分支。热加载先完整校验新快照再原子切换,写入中或非法版本继续使用上一份有效快照。Windows Node 是稳定的可信宿主,只负责持久化 job 目录、下载/上传、恢复、取消,以及从程序目录 `adapters/*/adapter.json` 发现 adapter;发布过程把对应源码合同复制进各 adapter 目录,Host 只读取 manifest 同目录指向的这一份合同并做通用 JSON Schema 校验,不再读取共享目录或采用本地优先 fallback。这样完整 Node 包与独立 adapter 包具有相同部署单元,manifest、合同、Worker 和依赖必须整体更新,避免旧合同静默覆盖新合同。manifest 声明 capability、实际 adapter 版本、运行类型、入口和契约文件,并可声明 1–1440 分钟的单任务超时;旧 manifest 缺省时保持 30 分钟,版本与时限不编译进 EXE。Node 在心跳中上报实际加载合同的 SHA-256 和 Workspace 协议版本,服务端只向明确兼容的节点调度 Workspace Job;具体软件探测和二次语义校验属于 Worker。Worker 可以是受管 Python 脚本或独立 EXE,进程只接收 job 目录,并以 `--probe` 返回运行状态,通过 `state.json`、`terminal.json` 与固定输出目录交接;Python、COM 或某个 SDK 都不属于通用协议。当前 Origin 和 ANSYS adapter 使用 Python 只是实现选择。
|
||||
|
||||
扩展现有 capability 的 feature 时修改共享契约、对应 Worker 和测试,不修改 Core 调度、Job 生命周期或 Node EXE;增加新专业软件时新增契约与一个 adapter 目录。Cloud 会在下一次契约查询时获得新校验和工具 schema;Node 在启动时从本地目录获得能力、版本和入口,并在首次连接及每次心跳中上报全部已发现 adapter。服务端用本次上报中自己具有共享契约的 capability 原子替换节点能力列表,未知能力不进入调度;管理端只读展示该结果,不保存人工能力选择。现有节点因此不需要清身份或重新注册,升级 Node 后重启一次即可在下一次心跳同步。当前选择明确的运维边界:zcbot 契约可热更新,不要求重启;本机 adapter 更新时先退出托盘 Node,整体替换 adapter 目录后重新启动,不重装、不替换 Node EXE,也不建设自动更新平台。当前 Node 仍按整机单执行槽保守串行,未来只有真实并行软件需求出现时,才把 slot 账本升级为 per-capability 租约,而不改变 Job 协议。
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@
|
|||
---
|
||||
## 已完成关键能力
|
||||
|
||||
- **08-19 / Unreleased / Windows Node adapter 合同单入口**:仓库根合同继续作为源码事实源,完整 Node 包与独立 adapter 包均在发布时把合同放入对应 adapter 目录;Node 移除共享 `software-contracts` fallback,心跳与诊断新增实际合同摘要和 Workspace 协议状态,服务端只向明确支持协议的节点派发 Workspace Job,避免遗留合同覆盖与确定性拒绝循环。
|
||||
|
||||
- **08-19 / Unreleased / Windows Node 持久 Workspace 与按需导出**:新增独立 `software_workspaces` 注册表,首个 Job 建立 Workspace、后续 Job 只从当前 head 在同一 home node 串行续作;Node 以 `workspaces/<workspace_id>/current + rollback` 保存两代状态,Origin adapter 1.0.0 可打开上一版 OPJU 再加工,失败自动回滚且不会按 Job 永久复制工程。Workspace 完成时只上传契约声明的预览缓存,预览不注册 Artifact;用户明确要求下载或交付时才由 `software_job_export` 触发原节点上传并登记正式产物。专项 101 项 unittest 与 .NET build 通过;全量 672 项仍仅 3 个既有数据库集成模块因显式测试库缺少 `users` 表未通过(另跳过 4 项),未连接或迁移生产数据库。
|
||||
|
||||
- **08-18 / Unreleased / Windows Node 能力自动同步**:注册码只建立节点身份,不再要求管理员选择 Origin/ANSYS 能力;Node 从 adapter registry 自动发现全部本机能力,并在首次连接及每次心跳上报,服务端以双方已有共享契约的能力覆盖节点调度列表,管理后台改为只读展示。现有节点无需清身份或重新注册,替换新版 Node 并重启一次即可同步。
|
||||
|
|
|
|||
2
RUN.md
2
RUN.md
|
|
@ -1137,7 +1137,7 @@ Web 用户登录后,文件栏 Job 中心会聚合本人最近任务。活动
|
|||
|
||||
常用二维扩展单图 type 包括 `area`、`polar`、`pie`,均使用 `input/x/y`;其中 polar 的 X 是角度(度)、Y 是半径,pie 的 X 是扇区标签、Y 是数值,两者使用 Origin 原生坐标/标签系统且不接受笛卡尔 `x_axis/y_axis/z_axis` 参数。`stacked_area`、`stacked_bar` 与 `stacked_column` 一样至少提供两条 `input/x/y`。堆叠系列可以来自不同输入,但行数和逐行 X 值必须完全一致,否则 Worker 在启动绘图前以 `STACKED_PLOT_X_VALUES_MISMATCH` 拒绝,避免错位累计。统计图与这些扩展图暂不混入 `multi_panel.series[].kind`。
|
||||
|
||||
专业软件契约位于 `software-contracts/*.json`。zcbot 会在文件变化后校验并热加载完整契约快照,正常修改契约不需要重启服务;非法或尚未写完的文件不会替换上一份有效快照。Node Host 从 EXE 同级的 `adapters/*/adapter.json` 发现本机能力,通用校验只读取 manifest 指向的 JSON Schema;完整包把所有共享契约放入 EXE 同级 `software-contracts/`,adapter 独立更新包也可携带同名契约副本。manifest 可用 `worker_timeout_minutes` 为每种软件声明 1–1440 分钟的执行上限;旧 manifest 未声明时继续使用 30 分钟。Worker 可以是受管 Python 脚本或独立 EXE。
|
||||
专业软件契约的源码事实源位于 `software-contracts/*.json`。zcbot 服务端会在文件变化后校验并热加载完整契约快照,正常修改契约不需要重启服务;非法或尚未写完的文件不会替换上一份有效快照。Node 发布时把对应事实源复制进 `adapters/<capability>/`,运行时只读取 manifest 同目录指向的合同,不存在共享目录 fallback;因此完整 Node 包与独立 adapter 包具有相同结构,更新时必须整体替换 adapter 目录并重启 Node。Node 心跳上报实际合同 SHA-256 和 Workspace 协议版本,服务端不会把 Workspace Job 派给未明确声明支持的节点。manifest 可用 `worker_timeout_minutes` 为每种软件声明 1–1440 分钟的执行上限;旧 manifest 未声明时继续使用 30 分钟。Worker 可以是受管 Python 脚本或独立 EXE。
|
||||
|
||||
两个 JSON 的职责不同:`adapter.json` 只描述本机如何启动(实际版本、runtime、入口和契约文件名),`origin.plot.v2.json` 描述云端与本机共同遵守的业务请求/输出契约。前者不能替代后者;独立打包脚本只是把根目录的同一份业务契约复制进 adapter 交付目录,不维护第二份源码。
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ from typing import Any
|
|||
|
||||
from jsonschema import Draft202012Validator, FormatChecker
|
||||
|
||||
|
||||
CONTRACT_ROOT = Path(__file__).resolve().parents[1] / "software-contracts"
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -355,10 +354,13 @@ def node_supports_request(
|
|||
if isinstance(advertised_features, list) and feature not in advertised_features:
|
||||
return False
|
||||
required_version = contract.required_adapter_version(request)
|
||||
if contract.workspace is not None and version_at_least(
|
||||
contract.workspace.required_adapter_version, required_version
|
||||
):
|
||||
required_version = contract.workspace.required_adapter_version
|
||||
if contract.workspace is not None:
|
||||
if int(capability_runtime.get("workspace_protocol_version") or 0) < 1:
|
||||
return False
|
||||
if version_at_least(
|
||||
contract.workspace.required_adapter_version, required_version
|
||||
):
|
||||
required_version = contract.workspace.required_adapter_version
|
||||
return version_at_least(actual_version, required_version)
|
||||
if contract.workspace is not None:
|
||||
return False
|
||||
|
|
@ -389,6 +391,11 @@ def node_available_slots(capability: str, runtime: dict[str, Any]) -> int:
|
|||
contract = get_contract(capability)
|
||||
item = (runtime.get("capability_runtime") or {}).get(capability)
|
||||
if isinstance(item, dict) and item.get("health") == "ready":
|
||||
if (
|
||||
contract.workspace is not None
|
||||
and int(item.get("workspace_protocol_version") or 0) < 1
|
||||
):
|
||||
return 0
|
||||
return max(0, int(item.get("available_slots") or 0))
|
||||
legacy_config = contract.legacy_runtime or {}
|
||||
if not legacy_config:
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from core.software_contracts import (
|
|||
DEFAULT_CAPABILITIES,
|
||||
_ContractRegistry,
|
||||
get_contract,
|
||||
node_available_slots,
|
||||
node_supports_request,
|
||||
version_at_least,
|
||||
)
|
||||
|
|
@ -62,12 +63,22 @@ class SoftwareContractTests(unittest.TestCase):
|
|||
"health": "ready",
|
||||
"available_slots": 1,
|
||||
"adapter_version": "1.0.0",
|
||||
"workspace_protocol_version": 1,
|
||||
"features": ["line", "heatmap"],
|
||||
}
|
||||
}
|
||||
}
|
||||
self.assertTrue(node_supports_request(contract, _request("heatmap"), current_runtime))
|
||||
self.assertEqual(node_available_slots("origin.plot@v2", current_runtime), 1)
|
||||
self.assertFalse(node_supports_request(contract, _request("bar"), current_runtime))
|
||||
current_runtime["capability_runtime"]["origin.plot@v2"].pop(
|
||||
"workspace_protocol_version"
|
||||
)
|
||||
self.assertFalse(node_supports_request(contract, _request("line"), current_runtime))
|
||||
self.assertEqual(node_available_slots("origin.plot@v2", current_runtime), 0)
|
||||
current_runtime["capability_runtime"]["origin.plot@v2"][
|
||||
"workspace_protocol_version"
|
||||
] = 1
|
||||
|
||||
multi_panel = _request("line")
|
||||
multi_panel["operation"]["plot"] = {
|
||||
|
|
|
|||
|
|
@ -716,6 +716,7 @@ class SoftwareJobProtocolTests(unittest.TestCase):
|
|||
node.capabilities = ["origin.plot@v2"]
|
||||
node.runtime = {"capability_runtime": {"origin.plot@v2": {
|
||||
"health": "ready", "available_slots": 1, "adapter_version": "1.0.0",
|
||||
"workspace_protocol_version": 1,
|
||||
"features": ["line"],
|
||||
}}}
|
||||
jobs = []
|
||||
|
|
|
|||
|
|
@ -203,8 +203,18 @@ class WindowsNodeSourceTests(unittest.TestCase):
|
|||
self.assertIn("..\\install-windows-node.bat", project)
|
||||
self.assertIn("..\\adapters\\**\\*", project)
|
||||
self.assertIn("adapters\\%(RecursiveDir)%(Filename)%(Extension)", project)
|
||||
self.assertIn("..\\..\\software-contracts\\*.json", project)
|
||||
self.assertIn("software-contracts\\%(Filename)%(Extension)", project)
|
||||
self.assertIn("..\\..\\software-contracts\\origin.plot.v2.json", project)
|
||||
self.assertIn("adapters\\origin.plot@v2\\origin.plot.v2.json", project)
|
||||
self.assertIn(
|
||||
"..\\..\\software-contracts\\ansys.mechanical.static_structural.v1.json",
|
||||
project,
|
||||
)
|
||||
self.assertIn(
|
||||
"adapters\\ansys.mechanical.static_structural@v1\\"
|
||||
"ansys.mechanical.static_structural.v1.json",
|
||||
project,
|
||||
)
|
||||
self.assertNotIn("software-contracts\\%(Filename)%(Extension)", project)
|
||||
self.assertFalse((ROOT / "install-windows-node.ps1").exists())
|
||||
self.assertFalse((ROOT / "install-origin-runtime.ps1").exists())
|
||||
self.assertFalse((ROOT / "install-startup.ps1").exists())
|
||||
|
|
@ -216,8 +226,11 @@ class WindowsNodeSourceTests(unittest.TestCase):
|
|||
self.assertIn('if /I "%~1"=="--self-contained"', script)
|
||||
self.assertIn("dotnet publish", script)
|
||||
self.assertIn('"install-windows-node.bat"', script)
|
||||
self.assertIn('for /r "!PUBLISH_DIR!\\adapters" %%F in (adapter.json)', script)
|
||||
self.assertIn('"!PUBLISH_DIR!\\software-contracts\\*.json"', script)
|
||||
self.assertIn('for /r "%PUBLISH_DIR%\\adapters" %%F in (adapter.json)', script)
|
||||
self.assertIn('do if exist "%%~fF"', script)
|
||||
self.assertIn("ADAPTER_CONTRACT_COUNT", script)
|
||||
self.assertIn("Published adapter has no colocated contract", script)
|
||||
self.assertNotIn('"!PUBLISH_DIR!\\software-contracts\\*.json"', script)
|
||||
self.assertIn("tar.exe -a -c -f", script)
|
||||
self.assertIn("certutil.exe -hashfile", script)
|
||||
self.assertFalse((ROOT / "package-windows-node.ps1").exists())
|
||||
|
|
@ -275,6 +288,9 @@ class WindowsNodeSourceTests(unittest.TestCase):
|
|||
'JsonSchema.Build(contractRoot.GetProperty("request_schema").Clone())',
|
||||
adapters,
|
||||
)
|
||||
self.assertIn("var contractPath = ResolveFile(root, manifest.Contract)", adapters)
|
||||
self.assertNotIn("ResolveContractFile", adapters)
|
||||
self.assertNotIn('AppContext.BaseDirectory, "software-contracts"', adapters)
|
||||
self.assertNotIn("IsValidOriginRequest", inbox)
|
||||
self.assertNotIn("IsValidPlot", inbox)
|
||||
self.assertIn('root.TryGetProperty("input_transfers"', inbox)
|
||||
|
|
@ -359,6 +375,13 @@ class WindowsNodeSourceTests(unittest.TestCase):
|
|||
self.assertIn('"..\\software-contracts\\origin.plot.v2.json"', script)
|
||||
self.assertNotIn("dotnet", script.lower())
|
||||
|
||||
form = (PROJECT / "ConfigurationForm.cs").read_text(encoding="utf-8")
|
||||
self.assertIn("Contract SHA-256: {adapter.ContractSha256}", form)
|
||||
self.assertIn("Workspace protocol:", form)
|
||||
self.assertIn("Workspace state:", form)
|
||||
self.assertIn("workspace_protocol_version", connection)
|
||||
self.assertIn("contract_sha256", connection)
|
||||
|
||||
uploader = (PROJECT / "JobOutputUploader.cs").read_text(encoding="utf-8")
|
||||
self.assertIn('new AuthenticationHeaderValue("Bearer", config.NodeToken)', uploader)
|
||||
self.assertIn('DefaultRequestHeaders.Add("X-Node-Id"', uploader)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
内网 MVP 的 Windows 执行节点,目标运行环境为 Windows 11 Enterprise + .NET 10 SDK 10.0.303。仓库根目录 `global.json` 固定 SDK patch;客户端只使用 .NET Windows Desktop Framework,不依赖第三方 NuGet 包。
|
||||
|
||||
当前实现托盘状态角标、配置与本机任务窗口、注册、DPAPI/ACL 配置保存、WebSocket `hello`/心跳和退避重连。Node Host 以语言无关的 job 目录协议负责持久化、下载、恢复、取消和上传,并从 EXE 同级 `adapters/*/adapter.json` 发现能力;Host 只按 manifest 指向的 JSON Schema 通用校验请求,软件探测、语义校验和执行都由 Worker 完成。Worker 可为受管 Python 脚本或独立 EXE,manifest 独立声明每种软件的任务超时及可选 Workspace 状态。当前安装包带 `origin.plot@v2` 和受真机验收门保护的 `ansys.mechanical.static_structural@v1` Python Worker,但 Python 不是 Node 通用协议的一部分。本机任务列表只读取已派发到该 Node 的持久化目录;Origin 工程保存在 `workspaces/<workspace-id>/current`,只保留一份 `rollback`,完成时自动上传轻量预览,用户明确请求导出时才上传并发布所选正式产物。
|
||||
当前实现托盘状态角标、配置与本机任务窗口、注册、DPAPI/ACL 配置保存、WebSocket `hello`/心跳和退避重连。Node Host 以语言无关的 job 目录协议负责持久化、下载、恢复、取消和上传,并从 EXE 同级 `adapters/*/adapter.json` 发现能力;每个 adapter 目录必须自带 manifest 指向的合同,Node 不读取共享合同目录。Host 只按该合同的 JSON Schema 通用校验请求,软件探测、语义校验和执行都由 Worker 完成。Worker 可为受管 Python 脚本或独立 EXE,manifest 独立声明每种软件的任务超时及可选 Workspace 状态。当前安装包带 `origin.plot@v2` 和受真机验收门保护的 `ansys.mechanical.static_structural@v1` Python Worker,但 Python 不是 Node 通用协议的一部分。本机任务列表只读取已派发到该 Node 的持久化目录;Origin 工程保存在 `workspaces/<workspace-id>/current`,只保留一份 `rollback`,完成时自动上传轻量预览,用户明确请求导出时才上传并发布所选正式产物。
|
||||
|
||||
在仓库根目录执行一条命令生成可分发 ZIP:
|
||||
|
||||
|
|
|
|||
|
|
@ -309,7 +309,13 @@ internal sealed class ConfigurationForm : Form
|
|||
.AppendLine($"Software: {runtime.Software} {runtime.SoftwareVersion ?? "unknown"}")
|
||||
.AppendLine($"Health: {runtime.Health}")
|
||||
.AppendLine($"Detail: {runtime.Detail}")
|
||||
.AppendLine($"Runtime: {adapter.RuntimePath}");
|
||||
.AppendLine($"Runtime: {adapter.RuntimePath}")
|
||||
.AppendLine($"Contract: {adapter.ContractPath}")
|
||||
.AppendLine($"Contract SHA-256: {adapter.ContractSha256}")
|
||||
.AppendLine(
|
||||
$"Workspace protocol: {(adapter.WorkspaceStateFilename is null ? "disabled" : "v1")}")
|
||||
.AppendLine(
|
||||
$"Workspace state: {adapter.WorkspaceStateFilename ?? "none"}");
|
||||
}
|
||||
catch (Exception exception) when (
|
||||
exception is IOException
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Json.Schema;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
|
|
@ -14,6 +15,8 @@ internal interface INodeAdapter
|
|||
string RunningDetail { get; }
|
||||
bool SupportsLocalAcceptance { get; }
|
||||
string RuntimePath { get; }
|
||||
string ContractPath { get; }
|
||||
string ContractSha256 { get; }
|
||||
string? WorkspaceStateFilename { get; }
|
||||
IReadOnlyList<string> PreviewOutputIds { get; }
|
||||
AdapterRuntimeStatus DetectRuntime();
|
||||
|
|
@ -52,6 +55,8 @@ internal sealed record NodeAdapterContract(
|
|||
internal sealed record AdapterDescriptor(
|
||||
string DirectoryPath,
|
||||
string EntrypointPath,
|
||||
string ContractPath,
|
||||
string ContractSha256,
|
||||
AdapterManifest Manifest,
|
||||
NodeAdapterContract Contract)
|
||||
{
|
||||
|
|
@ -92,8 +97,10 @@ internal sealed record AdapterDescriptor(
|
|||
throw new InvalidDataException("Executable adapter must not declare runtime_id.");
|
||||
}
|
||||
|
||||
var contractPath = ResolveContractFile(root, manifest.Contract);
|
||||
using var contractDocument = JsonDocument.Parse(File.ReadAllBytes(contractPath));
|
||||
var contractPath = ResolveFile(root, manifest.Contract);
|
||||
var contractBytes = File.ReadAllBytes(contractPath);
|
||||
var contractSha256 = Convert.ToHexString(SHA256.HashData(contractBytes)).ToLowerInvariant();
|
||||
using var contractDocument = JsonDocument.Parse(contractBytes);
|
||||
var contractRoot = contractDocument.RootElement;
|
||||
var capability = RequiredString(contractRoot, "capability");
|
||||
if (!capability.Equals(manifest.Capability, StringComparison.Ordinal))
|
||||
|
|
@ -138,6 +145,8 @@ internal sealed record AdapterDescriptor(
|
|||
return new AdapterDescriptor(
|
||||
root,
|
||||
entrypointPath,
|
||||
contractPath,
|
||||
contractSha256,
|
||||
manifest,
|
||||
new NodeAdapterContract(
|
||||
capability,
|
||||
|
|
@ -163,30 +172,6 @@ internal sealed record AdapterDescriptor(
|
|||
return resolved;
|
||||
}
|
||||
|
||||
private static string ResolveContractFile(string adapterRoot, string relativePath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(relativePath)
|
||||
|| Path.IsPathFullyQualified(relativePath)
|
||||
|| Path.GetFileName(relativePath) != relativePath)
|
||||
{
|
||||
throw new InvalidDataException("Adapter contract path must be a relative filename.");
|
||||
}
|
||||
var local = Path.GetFullPath(Path.Combine(adapterRoot, relativePath));
|
||||
if (local.StartsWith(adapterRoot + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)
|
||||
&& File.Exists(local))
|
||||
{
|
||||
return local;
|
||||
}
|
||||
var sharedRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "software-contracts"));
|
||||
var shared = Path.GetFullPath(Path.Combine(sharedRoot, relativePath));
|
||||
if (!shared.StartsWith(sharedRoot + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)
|
||||
|| !File.Exists(shared))
|
||||
{
|
||||
throw new InvalidDataException("Adapter contract is missing.");
|
||||
}
|
||||
return shared;
|
||||
}
|
||||
|
||||
private static void RequireOnlyProperties(JsonElement value, params string[] names)
|
||||
{
|
||||
if (value.ValueKind != JsonValueKind.Object)
|
||||
|
|
@ -269,6 +254,8 @@ internal sealed class ProcessNodeAdapter : INodeAdapter
|
|||
"ansys.mechanical.static_structural@v1", StringComparison.Ordinal)
|
||||
&& File.Exists(Path.Combine(Descriptor.DirectoryPath, "acceptance.py"));
|
||||
public string RuntimePath => runner.RuntimePath;
|
||||
public string ContractPath => Descriptor.ContractPath;
|
||||
public string ContractSha256 => Descriptor.ContractSha256;
|
||||
public string? WorkspaceStateFilename => Descriptor.Contract.WorkspaceStateFilename;
|
||||
public IReadOnlyList<string> PreviewOutputIds => Descriptor.Contract.PreviewOutputIds;
|
||||
|
||||
|
|
|
|||
|
|
@ -578,6 +578,8 @@ internal sealed class NodeConnectionLoop(NodeConfig config, Action<NodeStatus>?
|
|||
software = runtime.Software,
|
||||
software_version = runtime.SoftwareVersion,
|
||||
adapter_version = item.AdapterVersion,
|
||||
contract_sha256 = item.ContractSha256,
|
||||
workspace_protocol_version = item.WorkspaceStateFilename is null ? 0 : 1,
|
||||
features = item.Features,
|
||||
available_slots = slots,
|
||||
health = runtime.Health,
|
||||
|
|
|
|||
|
|
@ -20,8 +20,13 @@
|
|||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
</Content>
|
||||
<Content Include="..\..\software-contracts\*.json">
|
||||
<Link>software-contracts\%(Filename)%(Extension)</Link>
|
||||
<Content Include="..\..\software-contracts\origin.plot.v2.json">
|
||||
<Link>adapters\origin.plot@v2\origin.plot.v2.json</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
</Content>
|
||||
<Content Include="..\..\software-contracts\ansys.mechanical.static_structural.v1.json">
|
||||
<Link>adapters\ansys.mechanical.static_structural@v1\ansys.mechanical.static_structural.v1.json</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
</Content>
|
||||
|
|
|
|||
|
|
@ -70,15 +70,24 @@ for %%F in (
|
|||
)
|
||||
)
|
||||
set "ADAPTER_COUNT=0"
|
||||
for /r "!PUBLISH_DIR!\adapters" %%F in (adapter.json) do set /a ADAPTER_COUNT+=1
|
||||
for /r "%PUBLISH_DIR%\adapters" %%F in (adapter.json) do if exist "%%~fF" set /a ADAPTER_COUNT+=1
|
||||
if !ADAPTER_COUNT! LSS 1 (
|
||||
echo [ERR] Published package contains no adapters.
|
||||
goto :failed
|
||||
)
|
||||
if not exist "!PUBLISH_DIR!\software-contracts\*.json" (
|
||||
echo [ERR] Published package contains no software contracts.
|
||||
goto :failed
|
||||
set "ADAPTER_CONTRACT_ERROR=0"
|
||||
for /r "%PUBLISH_DIR%\adapters" %%F in (adapter.json) do if exist "%%~fF" (
|
||||
set "ADAPTER_DIR=%%~dpF"
|
||||
set "ADAPTER_CONTRACT_COUNT=0"
|
||||
for %%C in ("!ADAPTER_DIR!*.json") do (
|
||||
if exist "%%~fC" if /I not "%%~nxC"=="adapter.json" set /a ADAPTER_CONTRACT_COUNT+=1
|
||||
)
|
||||
if !ADAPTER_CONTRACT_COUNT! LSS 1 (
|
||||
echo [ERR] Published adapter has no colocated contract: %%F
|
||||
set "ADAPTER_CONTRACT_ERROR=1"
|
||||
)
|
||||
)
|
||||
if !ADAPTER_CONTRACT_ERROR! NEQ 0 goto :failed
|
||||
|
||||
tar.exe -a -c -f "!ARCHIVE_PATH!" -C "!OUTPUT_ROOT!" "!PACKAGE_NAME!"
|
||||
if errorlevel 1 (
|
||||
|
|
|
|||
Loading…
Reference in New Issue