using System; using System.Diagnostics; using System.Threading; using Aivfo.OperationLog; // D1-10/D3-04:control 侧操作日志(审计埋点) using IvfTl.Watchdog.Core; // D3-05:看门狗凭据缓存 / 停机标记 using ivf_tl_Control; // AppData, StartMain using IvfTl.Control.Services; // Log4netHelper(程序集 ivf_tl_Control_Services;切勿写 operate 端 ivf_tl_Services) namespace IvfTl.ControlHost { public static class Program { private static Mutex _singleton; private static ControlHttpServer _http; private static volatile bool _started; private static ManualResetEventSlim _exitEvent; [STAThread] public static int Main(string[] args) { // 1) 单实例:已有 control 在跑则立即退出(永远只有一个驱动机器)。 bool isNew; _singleton = new Mutex(true, @"Global\ivf_tl_control_singleton", out isNew); if (!isNew) { Log4netHelper.WriteLog("ControlHost: 已有实例在运行,本进程退出"); return 0; } try { var hostArgs = HostArgs.Parse(args); Log4netHelper.WriteLog($"ControlHost 启动 port={hostArgs.Port} account={hostArgs.Account}"); // D1-10/D3-04:先起操作日志组件(审计埋点),Project=control。紧接记一条"进程启动"生命周期审计 // (即刻入库,与硬件埋点共同构成 control 真实物理动作的合规审计)。全 try 兜底,绝不影响启动。 InitControlOperationLog(); try { OperationLogger.Log("生命周期", "control 进程启动", input: new { port = hostArgs.Port, account = hostArgs.Account, pid = Process.GetCurrentProcess().Id }, result: "成功"); } catch { } // 2) 先起 HTTP(让 operate 能尽早探到"在启动中"),started=false。 // 阶段2:/status rich + /serial/pause|resume + /shutdown。 _http = new ControlHttpServer( hostArgs.Port, BuildStatus, // /ping 轻量 BuildRichStatus, // /status 完整快照(§6 三块) HandleShutdown, // /shutdown 受护栏停机 HandleSerialPause, // /serial/pause 借串口让路 HandleSerialResume, // /serial/resume 归还恢复 msg => Log4netHelper.WriteLog(msg)); _http.Start(); // 3) 账号守卫(对齐 operate 空账号跳过逻辑)。 if (!hostArgs.IsValid) { Log4netHelper.WriteLog("ControlHost: 账号/密码为空,不启动采集(仅 HTTP 存活)"); } else { // 4) 启动序放后台线程(复刻 operate MainWindow 的 Task.Run 形态): // StartRun 内部会阻塞(InitTL 串口握手 + StartAsync().Wait()),不能占住主线程, // 否则下面的 _exitEvent.Wait() 永不可达、阶段2 /shutdown 也无从优雅停。 // 主线程只负责驻留;HTTP 在独立 Task 上,采集起没起都能被 operate 探活。 System.Threading.Tasks.Task.Run(() => RunStartupSequence(hostArgs)); } // 5) 驻留:主线程阻塞等退出信号(阶段2 的 /shutdown 会 Set 此事件)。 _exitEvent = new ManualResetEventSlim(false); _exitEvent.Wait(); return 0; } catch (Exception ex) { Log4netHelper.WriteLog("ControlHost 致命异常", ex); return 1; } finally { try { _http?.Stop(); } catch { } try { _singleton?.ReleaseMutex(); } catch { } } } /// /// control 启动序(后台线程跑,复刻 operate MainWindow 顺序,顺序不可变): /// Login → 设缓存盘 → HAL.ScanDevices → StartMain.StartRun。 /// 任一步失败仅记日志降级,不退进程(HTTP 仍存活,operate 可探活到"未就绪")。 /// private static void RunStartupSequence(HostArgs hostArgs) { try { if (!AppData.Instance.Login(hostArgs.Account, hostArgs.Password)) { Log4netHelper.WriteLog("ControlHost: control 登录失败"); return; } // D3-05:本次是"故意启动"(operate/看门狗/人显式拉起)→ 清故意停机标记,恢复看门狗守护; // 并把首次登录凭据 DPAPI 加密缓存到本地,供看门狗崩溃重拉时使用(明文不落盘)。全 try 兜底。 try { WatchdogPaths.SetStopped(false); } catch { } try { CredentialStore.Save(new Credentials { Account = hostArgs.Account, Password = hostArgs.Password, CacheDisk = hostArgs.CacheDisk }); Log4netHelper.WriteLog("[D3-05]control 登录凭据已加密缓存(供看门狗重拉)"); } catch (Exception cex) { Log4netHelper.WriteLog("[D3-05]凭据缓存失败(已忽略):" + cex.Message); } if (!string.IsNullOrEmpty(hostArgs.CacheDisk)) { ivf_tl_UtilHelper.PathHelper.pan = hostArgs.CacheDisk; AppData.Instance.LogService.Pan = hostArgs.CacheDisk; } try { IvfTl.Hardware.Impl.HardwareAccessLayer.Instance.Log = msg => Log4netHelper.WriteLog(msg); var devices = IvfTl.Hardware.Impl.HardwareAccessLayer.Instance.ScanDevices(); Log4netHelper.WriteLog($"ControlHost: HAL 发现 {devices.Count} 个舱"); } catch (Exception hex) { Log4netHelper.WriteLog("ControlHost: HAL 发现异常(降级):" + hex.Message); } var startMain = new StartMain(); string err = startMain.StartRun(); // 阻塞:InitTL→InitHouse→StartAsync if (!string.IsNullOrEmpty(err)) Log4netHelper.WriteLog("ControlHost: control 启动失败:" + err); else { _started = true; Log4netHelper.WriteLog("ControlHost: control 启动成功,常驻运行"); // D1-10:采集启动成功生命周期审计(带 tlSn)。 try { string tlSn = null; try { tlSn = AppData.Instance.TLSetting?.tlSn; } catch { } OperationLogger.Log("生命周期", "control 采集启动成功", output: new { tlSn, started = true }, result: "成功", tlSn: tlSn); } catch { } } } catch (Exception ex) { Log4netHelper.WriteLog("ControlHost: 启动序异常(降级,HTTP 仍存活)", ex); } } /// 提供给 HTTP /status 的快照(阶段1:基础存活;阶段2 接 GetMonitorSnapshot 补全)。 private static StatusDto BuildStatus() { string tlSn = ""; try { tlSn = AppData.Instance.TLSetting?.tlSn ?? ""; } catch { } return new StatusDto { Ok = true, Pid = Process.GetCurrentProcess().Id, TlSn = tlSn, Started = _started }; } /// /status 完整快照:基础存活 + control 现有 MonitorSnapshot(阶段2 §6 三块已在快照内补全)。 private static object BuildRichStatus() { var head = BuildStatus(); object snapshot = null; try { snapshot = AppData.Instance.GetMonitorSnapshot(); } catch { } return new { ok = head.Ok, pid = head.Pid, tlSn = head.TlSn, started = head.Started, snapshot }; } /// /shutdown 受护栏停机:校验工程师口令(App.config engineerPwd,默认 tl13579),通过则安全停机。 private static bool HandleShutdown(string token) { string engineerPwd = "tl13579"; try { var cfg = System.Configuration.ConfigurationManager.AppSettings["engineerPwd"]; if (!string.IsNullOrEmpty(cfg)) engineerPwd = cfg; } catch { } if (string.IsNullOrEmpty(token) || token != engineerPwd) { Log4netHelper.WriteLog("ControlHost: /shutdown 口令校验未通过,拒绝停机"); return false; } Log4netHelper.WriteLog("ControlHost: /shutdown 口令通过,执行安全停机"); // 异步触发,先把 HTTP 200 回给 operate,再停机退出。 System.Threading.Tasks.Task.Run(() => { try { System.Threading.Thread.Sleep(300); SafeShutdown(); } catch (Exception ex) { Log4netHelper.WriteLog("ControlHost: 停机异常", ex); _exitEvent?.Set(); } }); return true; } /// 统一安全停机:关相机/串口句柄(ShutdownAll)→ Set 退出事件(主线程 finally 停 HTTP+释放 Mutex)。 private static void SafeShutdown() { // D3-05:受护栏 /shutdown = 故意停机 → 写停机标记,看门狗见标记不重拉(直到下次故意启动清除)。 try { WatchdogPaths.SetStopped(true); Log4netHelper.WriteLog("[D3-05]已写故意停机标记,看门狗将不重拉"); } catch { } // D1-10:停机生命周期审计 + flush 操作日志(确保停机前在途审计落库),全 try 兜底。 try { OperationLogger.Log("生命周期", "control 安全停机", result: "成功"); } catch { } try { OperationLogger.Shutdown(); } catch { } try { IvfTl.Hardware.Impl.HardwareAccessLayer.Instance.ShutdownAll(); Log4netHelper.WriteLog("ControlHost: HardwareAccessLayer.ShutdownAll 完成"); } catch (Exception ex) { Log4netHelper.WriteLog("ControlHost: ShutdownAll 异常", ex); } _exitEvent?.Set(); } /// /// D1-10/D3-04:初始化 control 侧操作日志组件。Project=control,Kafka 取 App.config kfkaIP/kfkaPort,topic=tl-oplog /// (与 operate/front 同管道);ConfigFilePath=exe 同目录 oplog-config.json(≤15s 热加载,按模块开关)。 /// 全 try 兜底:日志初始化失败绝不影响 control 驱动机器。 /// private static void InitControlOperationLog() { try { string kafkaIp = System.Configuration.ConfigurationManager.AppSettings["kfkaIP"]?.ToString() ?? "127.0.0.1"; string kafkaPort = System.Configuration.ConfigurationManager.AppSettings["kfkaPort"]?.ToString() ?? "9092"; OperationLogger.Init(o => { o.Project = "control"; o.KafkaBootstrapServers = $"{kafkaIp}:{kafkaPort}"; o.Topic = "tl-oplog"; o.ConfigFilePath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "oplog-config.json"); }); Log4netHelper.WriteLog($"[D1-10]control 操作日志组件已初始化 kafka={kafkaIp}:{kafkaPort} topic=tl-oplog project=control"); } catch (Exception ex) { try { Log4netHelper.WriteLog($"[D1-10]control 操作日志组件初始化失败(已忽略):{ex.Message}"); } catch { } } } /// /serial/pause 借串口:置该舱 HouseGate 暂停标志,control 采集节拍据此让路(不驱动电机)。 private static bool HandleSerialPause(int houseSn) { try { var gate = IvfTl.Hardware.Impl.HardwareAccessLayer.Instance.GetHouseGate(houseSn); if (gate == null) return false; gate.PauseCapture(); Log4netHelper.WriteLog($"ControlHost: /serial/pause 舱{houseSn} 已让路(暂停采集)"); return true; } catch (Exception ex) { Log4netHelper.WriteLog($"ControlHost: /serial/pause 舱{houseSn} 异常", ex); return false; } } /// /serial/resume 归还:清该舱让路标志,恢复采集。 private static bool HandleSerialResume(int houseSn) { try { var gate = IvfTl.Hardware.Impl.HardwareAccessLayer.Instance.GetHouseGate(houseSn); if (gate == null) return false; gate.ResumeCapture(); Log4netHelper.WriteLog($"ControlHost: /serial/resume 舱{houseSn} 已恢复采集"); return true; } catch (Exception ex) { Log4netHelper.WriteLog($"ControlHost: /serial/resume 舱{houseSn} 异常", ex); return false; } } } }