07 · 宿主与进程 —— HostBridge / Mcp.Host / RuntimeCore.Host / Supervisor / Shell

前六章的能力都在库里。这一章讲怎么把它们跑起来、开出去:给前端(WebView2)、给外部 AI(MCP)、拆成受管子进程。这些都是顶层出口,自己不产生新能力,只转发下面的 Automation / Backend / ScriptRuntime。


Hmi.HostBridge —— 无头消息路由(前端的后端)

职责:一条 postMessage 进、一条 PostWebMessageAsJson 出的无头、可测路由核心。依赖 Automation(+ 运行态时的 Core/Backend/ScriptRuntime)。契约见 docs/frontend-contract 02。

路由核心 WebMessageRouter

src/Hmi.HostBridge/WebMessageRouter.cs(public sealed class)。吃一条入站信封 JSON,按 kind 分派:

  • rpc → AutomationRpcDispatcher(组态/感知)、回 rpc-result;
  • sub / unsub → TagSubscriptionBridge(运行态点值订阅);
  • tag-update 事件由订阅桥经 IWebMessageSink 主动推。

所有异常在此收敛成 rpc-result(ok:false),绝不让宿主因一条坏消息崩溃。 两种装配:①仅编辑器——只传 dispatcher,tags 桥为空,sub/unsub 回"运行态未就绪";②运行态——同时传订阅桥。

宿主侧管道(不是 Automation 能力,前端在 WebView2 里拿不到本机路径/进程,须由宿主接原生对话框):PickProjectDir、PickFile、PickSaveFile、LaunchRuntime、OpenProjectFolder、RunScript、PreviewStart / PreviewStop —— 都是可选注入的委托,未注入则对应 RPC 显式回"不就绪"。

组态/感知路由 AutomationRpcDispatcher

src/Hmi.HostBridge/AutomationRpcDispatcher.cs(public sealed class)。AutomationResult Dispatch(string method, JsonElement? payload) 把 method(= 契约方法名,如 get_project / list_screens / add_tag …)映射到 IProjectAutomation 的方法,从 JSON payload 取参。与 MCP 工具集是同一批操作的两个出口。未知 method 返回 Fail(不抛)。

runtime_write_tag 不在这里 —— 运行态 PLC 写值留阶段六,由路由器另行处理并强制确认。这条边界和第 06 章一脉相承。

点值订阅桥 TagSubscriptionBridge

src/Hmi.HostBridge/TagSubscriptionBridge.cs(public sealed class : ITagSubscriber, IDisposable)。web 发 sub/unsub 订阅一批点名,本桥订到 RTDB,把点变按批转成 tag-update 推给网页。

OnTagChanged 在 RTDB 分发线程只把批次拷走入内部队列就返回,真正的投影 + JSON 序列化 + Post 在本桥自己的工作线程上做(把序列化移出分发线程,否则一路 JSON 会成吞吐墙)。内部队列有界 8192 + DropOldest —— 实时显示只需最新帧,与整条显示车道语义一致。

其它分派器(同形,各管一域)

AuthRpcDispatcher(账号/权限,接 Hmi.Security)、HistoryQueryBridge(query_history / aggregate_history,不进 MCP)、AiRpcDispatcher(内置 AI 对话)、McpControlRpcDispatcher(MCP 服务器启停/配置)、UiOverrideBridge(脚本改界面 → 推 ui-prop-update)。出站统一经 IWebMessageSink。


Hmi.Mcp.Host —— 把能力开给外部 AI

职责:一个 exe,把 ProjectAutomation 的组态/感知方法包成 MCP 工具,交给外部 AI 客户端。依赖 Automation(+ ScriptRuntime 做脚本校验)。官方 MCP SDK。

入口 Program.cs —— 两种传输

Hmi.Mcp.Host <工程目录>                                      → stdio(Claude Desktop / Cursor 各自拉起)
Hmi.Mcp.Host <工程目录> --http <host> <port> [--token t]
                        [--read-only] [--tools a,b,c]         → http 常驻(本软件「MCP 服务器」面板启动)

工程目录也可经环境变量 HMI_PROJECT_DIR 指定。

⚠️ stdio 模式:stdout 是 JSON-RPC 通道,任何日志/诊断必须走 stderr,否则污染协议流。

配套类型

  • McpProjectHost.Open(projectDir) —— 打开工程 + 标记来源 + 接脚本编译校验器,一处收口(测试也走它,避免两个出口形状不一致)。
  • ProjectMcpTools —— 工具清单(逐个方法对应第 06 章的组态/感知能力)。
  • McpToolFilter —— --read-only(只留感知)/ --tools a,b,c(白名单)在这里落地。
  • McpArgs.Parse(args) —— 命令行解析。

边界不变:只组态 + 感知,没有 PLC 写。--read-only 再收一层,只剩只读。


Hmi.RuntimeCore.Host —— 运行内核子进程

职责:一个 exe,装配工程 + 起采集 + 挂历史/报警/脚本,作监督者的受管子进程。依赖 Backend、ScriptRuntime、Scripting、Project。

Hmi.RuntimeCore.Host <工程目录> [--crash-after-ready <ms>]

stdout 行协议(供 Hmi.Supervisor 判活):

  • 装配 + 起采集成功 → 打印恰好一行 RUNTIMECORE READY;
  • 之后每秒一行 HEARTBEAT <n>;
  • Ctrl+C / 父进程 Kill / stdin EOF → 干净停机(RuntimeHost.StopAsync);
  • --crash-after-ready <ms>:就绪后故意异常退出,仅供监督者"崩溃隔离/重启"测试。

内部就是把前几章接起来:ProjectLoader.Load → new RuntimeHost(project) → RuntimeDataServices.Compose(报警 + 历史订到 RTDB,历史库落工程 history/)→ host.Start() → 起脚本宿主(写值闸门:内部变量放行、通道点挡下;Comm.* 接 RuntimeHost 查在线/重连;Fs.* 围栏在工程 data/;Log.* → stderr)。热路径(RTDB + 采集)全在本进程内,不跨进程。


Hmi.Supervisor —— 进程管理器(监督者)

职责:仿力控——把实时库/日志等后台拆成独立进程集中托管,统一起停、无孤儿、崩溃重启。自成一体,无 Hmi.* 依赖。

入口 ProcessSupervisor : IDisposable

src/Hmi.Supervisor/ProcessSupervisor.cs:

成员 作用
StartSessionAsync(specs, ct) 按顺序拉起每个服务并各等就绪;任一就绪失败则回滚(停已起的)整体抛
StopSessionAsync(...) 一键全清
Dispose() 杀掉所有子进程(OS 保证回收,无孤儿)
bool IsRunning(name) / RunningServices 判活 / 列举(诊断)
Action<ServiceSpec,int>? ServiceAbandoned 某服务耗尽重启次数仍崩、被放弃时触发

崩溃时按 ServiceSpec.AutoRestart 重启或上报,故障不跨进程扩散。

服务描述 ServiceSpec

Name、ExePath、Arguments、WorkingDirectory、ReadyMarker(如 "RUNTIMECORE READY",子进程 stdout 打印含此串的一行即视就绪)、ReadyTimeout(默认 10s)、AutoRestart、MaxRestarts(默认 3)。单个子进程由 ManagedProcess 托管。

本层只做控制面(启停/就绪/心跳,走子进程 stdout 行协议);数据面(前端订点/写值)走前端 WebView2 那条路。


Hmi.Shell —— WPF 薄壳(编辑器/运行程序的窗)

职责:唯一的 net8.0-windows(WPF)程序集。装 WebView2、把网页和 WebMessageRouter 接起来。壳很薄,能力都在下面的库里。

  • App.xaml.cs —— 进程入口;--runtime <工程目录> 走运行态(带登录门禁),否则编辑器。
  • MainWindow.xaml.cs —— 建 WebView2、装虚拟主机加载前端、postMessage → Router.HandleInbound、把 WebView2Sink 接到路由器 sink;宿主侧管道(选目录/选文件/拉运行态/预览内核)在此实现并注入路由器。
  • WebView2Sink : IWebMessageSink —— 出站:路由器要发的 JSON 经 CoreWebView2.PostWebMessageAsJson 发出;订阅推送在 RTDB 分发线程回调,这里 marshal 回 UI 线程再调 WebView2(它只能在建它的线程上用)。
  • UiZoomStore、ShellLog。

编辑器进程以 configMode = true 起 AuthService(权限恒放行,见第 05 章);运行程序 --runtime 传 false,登录/权限/等级约束全生效。


小结 —— 三条出口

  • 给前端:WebMessageRouter + AutomationRpcDispatcher(+ 运行态 TagSubscriptionBridge),Hmi.Shell 装 WebView2 把它接活。
  • 给外部 AI:Hmi.Mcp.Host exe,stdio 或 http,McpToolFilter 收权限。
  • 拆进程托管:Hmi.RuntimeCore.Host 作子进程 + Hmi.Supervisor 监督,行协议判活、崩溃重启、无孤儿。

三条出口共用同一条安全边界:组态 + 感知放行,运行态 PLC 写值不从这些出口走。