|
@@ -0,0 +1,209 @@
|
|
|
|
|
+using System;
|
|
|
|
|
+using System.Diagnostics;
|
|
|
|
|
+using System.IO;
|
|
|
|
|
+using System.Net.Http;
|
|
|
|
|
+using System.Threading;
|
|
|
|
|
+using IvfTl.Watchdog.Core;
|
|
|
|
|
+
|
|
|
|
|
+namespace IvfTl.Watchdog
|
|
|
|
|
+{
|
|
|
|
|
+ /// <summary>
|
|
|
|
|
+ /// control 崩溃看门狗(D3-05)。常驻探活 control /ping,崩溃自动重拉(读 DPAPI 缓存凭据)。
|
|
|
|
|
+ /// 不依赖 operate 是否在跑。可 --pause/--resume(维护让路)、--stop(停这次)、--install/--uninstall(开机自启)。
|
|
|
|
|
+ /// </summary>
|
|
|
|
|
+ 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;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>常驻探活循环。</summary>
|
|
|
|
|
+ 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; }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>探活:control /ping 是否可达。</summary>
|
|
|
|
|
+ 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; }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>读缓存凭据,提权拉起 control(与 operate ControlProcessLauncher 同形态)。</summary>
|
|
|
|
|
+ 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; }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>control 可执行路径:默认与看门狗同目录(部署在同一 control\ 子目录)。</summary>
|
|
|
|
|
+ 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;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /// <summary>给常驻看门狗发停止信号(打开已存在的命名事件并置位);无常驻实例则静默。</summary>
|
|
|
|
|
+ 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 { }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|