using System; using System.Diagnostics; using System.IO; using System.Net.Http; using System.Threading; using IvfTl.Watchdog.Core; namespace IvfTl.Watchdog { /// /// control 崩溃看门狗(D3-05)。常驻探活 control /ping,崩溃自动重拉(读 DPAPI 缓存凭据)。 /// 不依赖 operate 是否在跑。可 --pause/--resume(维护让路)、--stop(停这次)、--install/--uninstall(开机自启)。 /// public static class Program { private const string MutexName = @"Global\ivf_tl_watchdog_singleton"; private const string StopEventName = @"Global\ivf_tl_watchdog_stop"; private const string RunKey = @"HKCU\Software\Microsoft\Windows\CurrentVersion\Run"; private const string RunValueName = "ivf_tl_watchdog"; private const int RelaunchCooldownSec = 30; // 两次重拉最小间隔(防崩溃风暴) private const int MaxConsecutiveFailures = 5; // 连续失败超此数 → 拉长冷却 + 告警 private const int LongCooldownSec = 300; private static readonly HttpClient _http = new HttpClient { Timeout = TimeSpan.FromSeconds(2) }; [STAThread] public static int Main(string[] args) { var a = WatchdogArgs.Parse(args); try { switch (a.Command) { case WatchdogCommand.Install: return DoInstall(a); case WatchdogCommand.Uninstall: return DoUninstall(); case WatchdogCommand.Pause: WatchdogPaths.SetPaused(true); Log("已暂停看门狗(只探活不重拉);--resume 恢复"); return 0; case WatchdogCommand.Resume: WatchdogPaths.SetPaused(false); Log("已恢复看门狗守护"); return 0; case WatchdogCommand.Stop: SignalStop(); Log("已向常驻看门狗发送停止信号"); return 0; default: return RunLoop(a); } } catch (Exception ex) { Log("看门狗致命异常:" + ex); return 1; } } /// 常驻探活循环。 private static int RunLoop(WatchdogArgs a) { bool isNew; using (var mutex = new Mutex(true, MutexName, out isNew)) { if (!isNew) { Log("已有看门狗实例在运行,本进程退出"); return 0; } using (var stopEvent = new EventWaitHandle(false, EventResetMode.ManualReset, StopEventName)) { Log($"看门狗启动 port={a.Port} interval={a.IntervalSec}s controlExe={ResolveControlExe()}"); int intervalMs = Math.Max(1, a.IntervalSec) * 1000; DateTime lastRelaunchUtc = DateTime.MinValue; int consecutiveFailures = 0; bool? lastAlive = null; // 状态变化才记日志,避免刷屏 while (true) { // 先判停止信号(--stop / --uninstall 触发):立刻优雅退出。 if (stopEvent.WaitOne(0)) { Log("收到停止信号,看门狗退出"); return 0; } bool alive = IsControlAlive(a.Port); if (alive != lastAlive) { Log(alive ? "control 存活(探活正常)" : "control 探活失败(不在)"); lastAlive = alive; } if (alive) { consecutiveFailures = 0; } else { bool paused = WatchdogPaths.IsPaused(); bool stopped = WatchdogPaths.IsDeliberatelyStopped(); int cooldown = consecutiveFailures >= MaxConsecutiveFailures ? LongCooldownSec : RelaunchCooldownSec; bool cooldownActive = (DateTime.UtcNow - lastRelaunchUtc).TotalSeconds < cooldown; if (RelaunchDecision.ShouldRelaunch(alive, paused, stopped, cooldownActive)) { var creds = CredentialStore.Load(); if (creds == null) { Log($"control 不在,但无可用缓存凭据(creds.dat 存在={File.Exists(WatchdogPaths.CredsFile)});等 operate 首次拉起 control 后再守护"); } else { lastRelaunchUtc = DateTime.UtcNow; consecutiveFailures++; bool ok = Relaunch(creds, a.Port); if (ok && consecutiveFailures >= MaxConsecutiveFailures) Log($"⚠ control 已连续 {consecutiveFailures} 次重拉仍未稳定,冷却拉长至 {LongCooldownSec}s,请人工检查"); } } else if (paused) Log("control 不在,但看门狗处于暂停(--resume 恢复),不重拉"); else if (stopped) Log("control 不在,但为故意停机(受护栏 /shutdown),不重拉"); // cooldown 中静默等待,不刷屏 } // 等一个周期;期间若收到停止信号则立刻醒来退出。 if (stopEvent.WaitOne(intervalMs)) { Log("收到停止信号,看门狗退出"); return 0; } } } } } /// 探活:control /ping 是否可达。 private static bool IsControlAlive(int port) { try { var resp = _http.GetAsync($"http://127.0.0.1:{port}/ping").GetAwaiter().GetResult(); return resp.IsSuccessStatusCode; } catch { return false; } } /// 读缓存凭据,提权拉起 control(与 operate ControlProcessLauncher 同形态)。 private static bool Relaunch(Credentials c, int port) { string exe = ResolveControlExe(); if (!File.Exists(exe)) { Log("找不到 control.exe:" + exe); return false; } try { var psi = new ProcessStartInfo { FileName = exe, Arguments = $"--account={c.Account} --password={c.Password} --cacheDisk={c.CacheDisk} --port={port}", UseShellExecute = true, // requireAdministrator 子进程需 ShellExecute(看门狗已是管理员,不弹 UAC) WindowStyle = ProcessWindowStyle.Hidden }; Process.Start(psi); Log($"control 不在 → 已重拉 control.exe(port={port})"); return true; } catch (Exception ex) { Log("重拉 control 失败:" + ex.Message); return false; } } /// control 可执行路径:默认与看门狗同目录(部署在同一 control\ 子目录)。 private static string ResolveControlExe() => Path.Combine(AppContext.BaseDirectory, "ivf_tl_ControlHost.exe"); // ── 控制面 ────────────────────────────────────────────── private static int DoInstall(WatchdogArgs a) { string exe = Process.GetCurrentProcess().MainModule.FileName; string cmd = $"\"{exe}\" --port={a.Port} --interval={a.IntervalSec}"; int rc = RunReg($"add \"{RunKey}\" /v {RunValueName} /t REG_SZ /d \"{cmd}\" /f"); Log(rc == 0 ? $"已写开机自启:{cmd}" : "写开机自启失败 rc=" + rc); return rc; } private static int DoUninstall() { // 先停常驻实例,再删自启项 = 一条命令彻底卸载、无残留。 SignalStop(); int rc = RunReg($"delete \"{RunKey}\" /v {RunValueName} /f"); Log(rc == 0 ? "已删除开机自启项,看门狗已卸载" : "删除开机自启项(可能本就不存在) rc=" + rc); return 0; } /// 给常驻看门狗发停止信号(打开已存在的命名事件并置位);无常驻实例则静默。 private static void SignalStop() { try { using (var ev = EventWaitHandle.OpenExisting(StopEventName)) ev.Set(); } catch (WaitHandleCannotBeOpenedException) { Log("无常驻看门狗在运行(无需停止)"); } catch (Exception ex) { Log("发送停止信号异常:" + ex.Message); } } private static int RunReg(string arguments) { try { var psi = new ProcessStartInfo("reg.exe", arguments) { UseShellExecute = false, CreateNoWindow = true }; var p = Process.Start(psi); p.WaitForExit(); return p.ExitCode; } catch (Exception ex) { Log("reg.exe 执行异常:" + ex.Message); return -1; } } // ── 日志 ──────────────────────────────────────────────── private static readonly object _logLock = new object(); private static void Log(string msg) { string line = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} {msg}"; try { lock (_logLock) { WatchdogPaths.EnsureDataDir(); File.AppendAllText(Path.Combine(WatchdogPaths.DataDir, "watchdog.log"), line + Environment.NewLine); } } catch { } try { Console.WriteLine(line); } catch { } } } }