Program.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. using System;
  2. using System.Diagnostics;
  3. using System.Threading;
  4. using Aivfo.OperationLog; // D1-10/D3-04:control 侧操作日志(审计埋点)
  5. using IvfTl.Watchdog.Core; // D3-05:看门狗凭据缓存 / 停机标记
  6. using ivf_tl_Control; // AppData, StartMain
  7. using IvfTl.Control.Services; // Log4netHelper(程序集 ivf_tl_Control_Services;切勿写 operate 端 ivf_tl_Services)
  8. namespace IvfTl.ControlHost
  9. {
  10. public static class Program
  11. {
  12. private static Mutex _singleton;
  13. private static ControlHttpServer _http;
  14. private static volatile bool _started;
  15. private static ManualResetEventSlim _exitEvent;
  16. [STAThread]
  17. public static int Main(string[] args)
  18. {
  19. // 1) 单实例:已有 control 在跑则立即退出(永远只有一个驱动机器)。
  20. bool isNew;
  21. _singleton = new Mutex(true, @"Global\ivf_tl_control_singleton", out isNew);
  22. if (!isNew)
  23. {
  24. Log4netHelper.WriteLog("ControlHost: 已有实例在运行,本进程退出");
  25. return 0;
  26. }
  27. try
  28. {
  29. var hostArgs = HostArgs.Parse(args);
  30. Log4netHelper.WriteLog($"ControlHost 启动 port={hostArgs.Port} account={hostArgs.Account}");
  31. // D1-10/D3-04:先起操作日志组件(审计埋点),Project=control。紧接记一条"进程启动"生命周期审计
  32. // (即刻入库,与硬件埋点共同构成 control 真实物理动作的合规审计)。全 try 兜底,绝不影响启动。
  33. InitControlOperationLog();
  34. try
  35. {
  36. OperationLogger.Log("生命周期", "control 进程启动",
  37. input: new { port = hostArgs.Port, account = hostArgs.Account, pid = Process.GetCurrentProcess().Id },
  38. result: "成功");
  39. }
  40. catch { }
  41. // 2) 先起 HTTP(让 operate 能尽早探到"在启动中"),started=false。
  42. // 阶段2:/status rich + /serial/pause|resume + /shutdown。
  43. var debugMgr = new IvfTl.ControlHost.Debug.DebugSessionManager(
  44. houseSn => IvfTl.Hardware.Impl.HardwareAccessLayer.Instance.GetHouseGate(houseSn),
  45. () => DateTime.UtcNow,
  46. ttlMs: 10000,
  47. log: msg => Log4netHelper.WriteLog(msg),
  48. cultivationOf: houseSn => { try { return ivf_tl_Control.AppData.Instance.GetCultivation(houseSn); } catch { return (false, 0); } });
  49. // D2-02 Task3.2a:标定协作管理器(按 sid 管 CalibrationCoordinator,逐 well 接真实引擎/落库)。
  50. // 取 HouseBin 用注入委托(避免直接静态依赖,便于测试);标定全程复用 debug 会话的 lease,不另借硬件。
  51. var calibMgr = new IvfTl.ControlHost.Debug.CalibrationManager(
  52. debugMgr,
  53. houseSn => { try { return ivf_tl_Control.AppData.Instance.GetHouseBin(houseSn); } catch { return null; } },
  54. msg => Log4netHelper.WriteLog(msg));
  55. _http = new ControlHttpServer(
  56. hostArgs.Port,
  57. BuildStatus, // /ping 轻量
  58. BuildRichStatus, // /status 完整快照(§6 三块)
  59. HandleShutdown, // /shutdown 受护栏停机
  60. HandleSerialPause, // /serial/pause 借串口让路
  61. HandleSerialResume, // /serial/resume 归还恢复
  62. msg => Log4netHelper.WriteLog(msg), debugMgr, calibMgr);
  63. _http.Start();
  64. // 3) 账号守卫(对齐 operate 空账号跳过逻辑)。
  65. if (!hostArgs.IsValid)
  66. {
  67. Log4netHelper.WriteLog("ControlHost: 账号/密码为空,不启动采集(仅 HTTP 存活)");
  68. }
  69. else
  70. {
  71. // 4) 启动序放后台线程(复刻 operate MainWindow 的 Task.Run 形态):
  72. // StartRun 内部会阻塞(InitTL 串口握手 + StartAsync().Wait()),不能占住主线程,
  73. // 否则下面的 _exitEvent.Wait() 永不可达、阶段2 /shutdown 也无从优雅停。
  74. // 主线程只负责驻留;HTTP 在独立 Task 上,采集起没起都能被 operate 探活。
  75. System.Threading.Tasks.Task.Run(() => RunStartupSequence(hostArgs));
  76. }
  77. // 5) 驻留:主线程阻塞等退出信号(阶段2 的 /shutdown 会 Set 此事件)。
  78. _exitEvent = new ManualResetEventSlim(false);
  79. // TTL 看门狗:周期 SweepExpired 回收超时调试会话(spec §5.1,绝不指望 operate 主动还)。
  80. System.Threading.Tasks.Task.Run(async () =>
  81. {
  82. while (!(_exitEvent?.IsSet ?? false))
  83. {
  84. try { debugMgr.SweepExpired(); } catch (Exception ex) { Log4netHelper.WriteLog("[debug] Sweep 异常:" + ex.Message); }
  85. await System.Threading.Tasks.Task.Delay(3000);
  86. }
  87. });
  88. _exitEvent.Wait();
  89. return 0;
  90. }
  91. catch (Exception ex)
  92. {
  93. Log4netHelper.WriteLog("ControlHost 致命异常", ex);
  94. return 1;
  95. }
  96. finally
  97. {
  98. try { _http?.Stop(); } catch { }
  99. try { _singleton?.ReleaseMutex(); } catch { }
  100. }
  101. }
  102. /// <summary>
  103. /// control 启动序(后台线程跑,复刻 operate MainWindow 顺序,顺序不可变):
  104. /// Login → 设缓存盘 → HAL.ScanDevices → StartMain.StartRun。
  105. /// 任一步失败仅记日志降级,不退进程(HTTP 仍存活,operate 可探活到"未就绪")。
  106. /// </summary>
  107. private static void RunStartupSequence(HostArgs hostArgs)
  108. {
  109. try
  110. {
  111. if (!AppData.Instance.Login(hostArgs.Account, hostArgs.Password))
  112. {
  113. Log4netHelper.WriteLog("ControlHost: control 登录失败");
  114. return;
  115. }
  116. // D3-05:本次是"故意启动"(operate/看门狗/人显式拉起)→ 清故意停机标记,恢复看门狗守护;
  117. // 并把首次登录凭据 DPAPI 加密缓存到本地,供看门狗崩溃重拉时使用(明文不落盘)。全 try 兜底。
  118. try { WatchdogPaths.SetStopped(false); } catch { }
  119. try
  120. {
  121. CredentialStore.Save(new Credentials
  122. {
  123. Account = hostArgs.Account,
  124. Password = hostArgs.Password,
  125. CacheDisk = hostArgs.CacheDisk
  126. });
  127. Log4netHelper.WriteLog("[D3-05]control 登录凭据已加密缓存(供看门狗重拉)");
  128. }
  129. catch (Exception cex) { Log4netHelper.WriteLog("[D3-05]凭据缓存失败(已忽略):" + cex.Message); }
  130. if (!string.IsNullOrEmpty(hostArgs.CacheDisk))
  131. {
  132. ivf_tl_UtilHelper.PathHelper.pan = hostArgs.CacheDisk;
  133. AppData.Instance.LogService.Pan = hostArgs.CacheDisk;
  134. }
  135. try
  136. {
  137. IvfTl.Hardware.Impl.HardwareAccessLayer.Instance.Log =
  138. msg => Log4netHelper.WriteLog(msg);
  139. var devices = IvfTl.Hardware.Impl.HardwareAccessLayer.Instance.ScanDevices();
  140. Log4netHelper.WriteLog($"ControlHost: HAL 发现 {devices.Count} 个舱");
  141. }
  142. catch (Exception hex)
  143. {
  144. Log4netHelper.WriteLog("ControlHost: HAL 发现异常(降级):" + hex.Message);
  145. }
  146. var startMain = new StartMain();
  147. string err = startMain.StartRun(); // 阻塞:InitTL→InitHouse→StartAsync
  148. if (!string.IsNullOrEmpty(err))
  149. Log4netHelper.WriteLog("ControlHost: control 启动失败:" + err);
  150. else
  151. {
  152. _started = true;
  153. Log4netHelper.WriteLog("ControlHost: control 启动成功,常驻运行");
  154. // D1-10:采集启动成功生命周期审计(带 tlSn)。
  155. try
  156. {
  157. string tlSn = null;
  158. try { tlSn = AppData.Instance.TLSetting?.tlSn; } catch { }
  159. OperationLogger.Log("生命周期", "control 采集启动成功",
  160. output: new { tlSn, started = true }, result: "成功", tlSn: tlSn);
  161. }
  162. catch { }
  163. }
  164. }
  165. catch (Exception ex)
  166. {
  167. Log4netHelper.WriteLog("ControlHost: 启动序异常(降级,HTTP 仍存活)", ex);
  168. }
  169. }
  170. /// <summary>提供给 HTTP /status 的快照(阶段1:基础存活;阶段2 接 GetMonitorSnapshot 补全)。</summary>
  171. private static StatusDto BuildStatus()
  172. {
  173. string tlSn = "";
  174. try { tlSn = AppData.Instance.TLSetting?.tlSn ?? ""; } catch { }
  175. return new StatusDto
  176. {
  177. Ok = true,
  178. Pid = Process.GetCurrentProcess().Id,
  179. TlSn = tlSn,
  180. Started = _started
  181. };
  182. }
  183. /// <summary>/status 完整快照:基础存活 + control 现有 MonitorSnapshot(阶段2 §6 三块已在快照内补全)。</summary>
  184. private static object BuildRichStatus()
  185. {
  186. var head = BuildStatus();
  187. object snapshot = null;
  188. try { snapshot = AppData.Instance.GetMonitorSnapshot(); } catch { }
  189. return new
  190. {
  191. ok = head.Ok,
  192. pid = head.Pid,
  193. tlSn = head.TlSn,
  194. started = head.Started,
  195. snapshot
  196. };
  197. }
  198. /// <summary>/shutdown 受护栏停机:校验工程师口令(App.config engineerPwd,默认 tl13579),通过则安全停机。</summary>
  199. private static bool HandleShutdown(string token)
  200. {
  201. string engineerPwd = "tl13579";
  202. try
  203. {
  204. var cfg = System.Configuration.ConfigurationManager.AppSettings["engineerPwd"];
  205. if (!string.IsNullOrEmpty(cfg)) engineerPwd = cfg;
  206. }
  207. catch { }
  208. if (string.IsNullOrEmpty(token) || token != engineerPwd)
  209. {
  210. Log4netHelper.WriteLog("ControlHost: /shutdown 口令校验未通过,拒绝停机");
  211. return false;
  212. }
  213. Log4netHelper.WriteLog("ControlHost: /shutdown 口令通过,执行安全停机");
  214. // 异步触发,先把 HTTP 200 回给 operate,再停机退出。
  215. System.Threading.Tasks.Task.Run(() =>
  216. {
  217. try { System.Threading.Thread.Sleep(300); SafeShutdown(); }
  218. catch (Exception ex) { Log4netHelper.WriteLog("ControlHost: 停机异常", ex); _exitEvent?.Set(); }
  219. });
  220. return true;
  221. }
  222. /// <summary>统一安全停机:关相机/串口句柄(ShutdownAll)→ Set 退出事件(主线程 finally 停 HTTP+释放 Mutex)。</summary>
  223. private static void SafeShutdown()
  224. {
  225. // D3-05:受护栏 /shutdown = 故意停机 → 写停机标记,看门狗见标记不重拉(直到下次故意启动清除)。
  226. try { WatchdogPaths.SetStopped(true); Log4netHelper.WriteLog("[D3-05]已写故意停机标记,看门狗将不重拉"); } catch { }
  227. // D1-10:停机生命周期审计 + flush 操作日志(确保停机前在途审计落库),全 try 兜底。
  228. try { OperationLogger.Log("生命周期", "control 安全停机", result: "成功"); } catch { }
  229. try { OperationLogger.Shutdown(); } catch { }
  230. try
  231. {
  232. IvfTl.Hardware.Impl.HardwareAccessLayer.Instance.ShutdownAll();
  233. Log4netHelper.WriteLog("ControlHost: HardwareAccessLayer.ShutdownAll 完成");
  234. }
  235. catch (Exception ex) { Log4netHelper.WriteLog("ControlHost: ShutdownAll 异常", ex); }
  236. _exitEvent?.Set();
  237. }
  238. /// <summary>
  239. /// D1-10/D3-04:初始化 control 侧操作日志组件。Project=control,Kafka 取 App.config kfkaIP/kfkaPort,topic=tl-oplog
  240. /// (与 operate/front 同管道);ConfigFilePath=exe 同目录 oplog-config.json(≤15s 热加载,按模块开关)。
  241. /// 全 try 兜底:日志初始化失败绝不影响 control 驱动机器。
  242. /// </summary>
  243. private static void InitControlOperationLog()
  244. {
  245. try
  246. {
  247. string kafkaIp = System.Configuration.ConfigurationManager.AppSettings["kfkaIP"]?.ToString() ?? "127.0.0.1";
  248. string kafkaPort = System.Configuration.ConfigurationManager.AppSettings["kfkaPort"]?.ToString() ?? "9092";
  249. OperationLogger.Init(o =>
  250. {
  251. o.Project = "control";
  252. o.KafkaBootstrapServers = $"{kafkaIp}:{kafkaPort}";
  253. o.Topic = "tl-oplog";
  254. o.ConfigFilePath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "oplog-config.json");
  255. });
  256. Log4netHelper.WriteLog($"[D1-10]control 操作日志组件已初始化 kafka={kafkaIp}:{kafkaPort} topic=tl-oplog project=control");
  257. }
  258. catch (Exception ex)
  259. {
  260. try { Log4netHelper.WriteLog($"[D1-10]control 操作日志组件初始化失败(已忽略):{ex.Message}"); } catch { }
  261. }
  262. }
  263. /// <summary>/serial/pause 借串口:置该舱 HouseGate 暂停标志,control 采集节拍据此让路(不驱动电机)。</summary>
  264. private static bool HandleSerialPause(int houseSn)
  265. {
  266. try
  267. {
  268. var gate = IvfTl.Hardware.Impl.HardwareAccessLayer.Instance.GetHouseGate(houseSn);
  269. if (gate == null) return false;
  270. gate.PauseCapture();
  271. Log4netHelper.WriteLog($"ControlHost: /serial/pause 舱{houseSn} 已让路(暂停采集)");
  272. return true;
  273. }
  274. catch (Exception ex) { Log4netHelper.WriteLog($"ControlHost: /serial/pause 舱{houseSn} 异常", ex); return false; }
  275. }
  276. /// <summary>/serial/resume 归还:清该舱让路标志,恢复采集。</summary>
  277. private static bool HandleSerialResume(int houseSn)
  278. {
  279. try
  280. {
  281. var gate = IvfTl.Hardware.Impl.HardwareAccessLayer.Instance.GetHouseGate(houseSn);
  282. if (gate == null) return false;
  283. gate.ResumeCapture();
  284. Log4netHelper.WriteLog($"ControlHost: /serial/resume 舱{houseSn} 已恢复采集");
  285. return true;
  286. }
  287. catch (Exception ex) { Log4netHelper.WriteLog($"ControlHost: /serial/resume 舱{houseSn} 异常", ex); return false; }
  288. }
  289. }
  290. }