Оповещения: группа «Администратор системы», блок «Кому писать» открыт; копии: адрес хранилища как в проводнике

Администратору (адрес из «Почты») — сбои (ИШ, связь с НРД, службы bj, DEPO) и обновления (новый выпуск bj,
ИШ, лицензия, сертификаты); отделам — переводы и поручения. Существующие установки: администратор получает
всё, что получал. Новые оповещения: «Доступно обновление bj» (раз на выпуск), «Служба … не работает /
снова работает» (модули и служба ключей Валидаты). Резервные копии: \\сервер\папка или сервер + общая
папка по полям → //сервер/папка; понятная причина отказа доступа.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
This commit is contained in:
zuevav
2026-10-06 22:44:35 +03:00
parent f07bb53162
commit d23ddf6e0c
16 changed files with 547 additions and 63 deletions
+126
View File
@@ -0,0 +1,126 @@
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)
}
}
}