#!/usr/bin/env node "use strict"; const fs = require("node:fs"); const path = require("node:path"); const { transition } = require("./state"); const { sendSmtpMail } = require("../lake-lamp-authz/smtp-mailer"); async function main() { const config = JSON.parse(fs.readFileSync(process.env.SENTINEL_CONFIG || "/etc/guanghu/state-change-sentinel.json", "utf8")); const stateFile = process.env.SENTINEL_STATE || "/var/lib/guanghu/state-change-sentinel/state.json"; const previous = fs.existsSync(stateFile) ? JSON.parse(fs.readFileSync(stateFile, "utf8")) : {}; const next = {}; const notices = []; for (const target of config.targets) { const current = await check(target); next[target.id] = current; const changed = transition(previous[target.id], current); if (changed.notify) notices.push({ id: target.id, kind: changed.kind, detail: current.detail }); } fs.mkdirSync(path.dirname(stateFile), { recursive: true, mode: 0o700 }); fs.writeFileSync(stateFile, JSON.stringify(next), { mode: 0o600 }); if (!notices.length) return; const lines = notices.map(item => `${item.kind === "anomaly" ? "异常" : "恢复"}: ${item.id} · ${item.detail}`); await sendSmtpMail({ to: process.env.AUTH_RECIPIENT, subject: `光湖节点状态变化 · ${notices.map(item => item.id).join(", ")}`, text: ["光湖节点状态发生变化。静止在线期间不会发送邮件。", "", ...lines, "", new Date().toISOString()].join("\n"), smtpHost: process.env.SMTP_HOST || "smtp.qq.com", smtpPort: Number(process.env.SMTP_PORT || 465), smtpUser: process.env.SMTP_USER, smtpPass: process.env.SMTP_PASS, }); } async function check(target) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), target.timeout_ms || 8000); try { const response = await fetch(target.url, { method: "GET", redirect: "manual", signal: controller.signal, headers: { "user-agent": "Guanghu-State-Change-Sentinel/1.0" } }); const ok = (target.accept || [200]).includes(response.status); return { ok, detail: `HTTP ${response.status}`, observed_at: new Date().toISOString() }; } catch (error) { return { ok: false, detail: error.name === "AbortError" ? "timeout" : "connection-failed", observed_at: new Date().toISOString() }; } finally { clearTimeout(timer); } } main().catch(error => { process.stderr.write(`state sentinel failed: ${error.message}\n`); process.exit(1); });