package lkgateway // unitswatch.go — сторож служб (06.10.2026): администратор системы узнаёт письмом, что служба модуля // (bj-crypto, bj-npr, bj-quikexport, bj-lk) или служба ключей Валидаты не работает, — раньше это было видно // только в карточке «Службы» или по ошибкам раздела. ИШ НРД сторожит igwwatch, сбои внутри bj-server — health. // Служба должна лежать две проверки подряд: перезапуск при обновлении письма не вызывает. import ( "context" "os" "os/exec" "path/filepath" "strings" "sync" "time" ) const unitsCheckEvery = 2 * time.Minute var unitsWatch = struct { sync.Mutex down map[string]int // служба → проверок подряд, когда она не работала alerted map[string]bool // о службе уже написали }{down: map[string]int{}, alerted: map[string]bool{}} // watchedUnit — служба под присмотром и её состояние сейчас. type watchedUnit struct { Key, Title, Unit string OK bool State string // как она лежит — для письма Hint string // что из-за этого не работает и как поднять } func (s *Server) unitsWatchLoop(ctx context.Context) { healthRegister("units-watch", 2*unitsCheckEvery) t := time.NewTicker(unitsCheckEvery) defer t.Stop() for { unitsWatchTick(s.rc, watchedUnits(s.rc)) healthBeat("units-watch") select { case <-ctx.Done(): return case <-t.C: } } } // watchedUnits — службы модулей этого сервера (кроме самого bj-server) и служба ключей Валидаты. func watchedUnits(rc *RuntimeConfig) []watchedUnit { var out []watchedUnit for _, v := range unitViews(rc) { if v.Unit == "bj-server" { continue } st := v.State + "/" + v.Sub if v.Result != "" { st += ", причина: " + v.Result } if v.Since != "" { st += ", с " + v.Since } out = append(out, watchedUnit{Key: v.Unit, Title: v.Title, Unit: v.Unit, OK: v.OK(), State: st, Hint: "Перезапустить — Настройка → Диагностика → «Службы», кнопка «Перезапустить»; если не поднимается — journalctl -u " + v.Unit + "."}) } // служба ключей — когда она нужна: включена в автозапуск (bj включает её при установке ключа) или уже работала; // на свежем сервере до установки ключа её может и не быть — это не сбой if _, err := os.Stat("/opt/Validata/VDCSP/bin/amd64/vdcrysvc"); err == nil { ok := procRunning("vdcrysvc") unitsWatch.Lock() seen := unitsWatch.alerted["vdcrysvc"] || unitsWatch.down["vdcrysvc"] > 0 || vdcrysvcSeen if ok { vdcrysvcSeen = true } unitsWatch.Unlock() if !ok && !seen && !unitEnabled("vdmkdev.service") { return out } out = append(out, watchedUnit{Key: "vdcrysvc", Title: "служба ключей Валидаты", Unit: "vdmkdev", OK: ok, State: "процесс vdcrysvc не запущен", Hint: "Без неё ключ подписи не работает: шлюз НРД не подпишет и не отправит документы. Запустить: sudo /etc/init.d/vdmkdev start."}) } return out } // vdcrysvcSeen — служба ключей Валидаты работала с запуска bj (под unitsWatch). var vdcrysvcSeen bool // unitEnabled — служба включена в автозапуск (systemctl is-enabled, без прав root). func unitEnabled(unit string) bool { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() out, _ := exec.CommandContext(ctx, "systemctl", "is-enabled", unit).Output() return strings.TrimSpace(string(out)) == "enabled" } // procRunning — есть ли процесс с таким именем (по /proc/*/comm, без прав root). func procRunning(name string) bool { dirs, _ := filepath.Glob("/proc/[0-9]*/comm") for _, f := range dirs { if b, err := os.ReadFile(f); err == nil && strings.TrimSpace(string(b)) == name { return true } } return false } func unitsWatchTick(rc *RuntimeConfig, units []watchedUnit) { unitsWatch.Lock() defer unitsWatch.Unlock() for _, u := range units { if u.OK { if unitsWatch.alerted[u.Key] { raiseSystemAlert(rc, "Служба «"+u.Title+"» снова работает", "Служба "+u.Unit+" ("+u.Title+") снова запущена.") } delete(unitsWatch.down, u.Key) delete(unitsWatch.alerted, u.Key) continue } unitsWatch.down[u.Key]++ if unitsWatch.down[u.Key] >= 2 && !unitsWatch.alerted[u.Key] { unitsWatch.alerted[u.Key] = true raiseSystemAlert(rc, "Служба «"+u.Title+"» не работает", "Служба "+u.Unit+" ("+u.Title+") не работает: "+u.State+". "+u.Hint) } } }