Files
Bridge-and-Join-s/internal/lkgateway/nsdlinkwatch.go
T
zuevav afc5776347 Индикатор «Шлюз НРД», проверка ключа ИШ, папки шлюза по сети, сотрудники из домена, переключатель QUIK в настройках
- журнал ИШ: номер процесса выравнивается пробелами — опросы НРД не распознавались (серый индикатор)
- «Проверить СКЗИ и ключ»: Валидата, датчик случайных чисел, подпись профилем ИШ (а не старый bj-crypto);
  индикатор «Криптографии» — по готовности ключа; карточка автозагрузки УЦ — только для разработки
- шаг «Шлюз НРД»: папки обмена по сети (Samba): логин/пароль, ручная настройка не трогается
- «Учётные записи»: необязательно — подключить домен, проверить связь, найти сотрудника и добавить с ролью;
  роль из bj действует, пока группы домена с ролями не сопоставлены
- настройка QUIK МНП: «Только чтение / Чтение и запись», тонкая настройка цикла свёрнута
- индикатор «Обновления» — только когда есть что обновить; «Мастер настройки» в меню и в настройках

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
2026-10-07 00:24:19 +03:00

183 lines
7.6 KiB
Go

package lkgateway
// Сторож связи ИШ ↔ НРД. ИШ может отвечать bj, но сам не достукиваться до НРД (контур НРД отдаёт
// 404/500/502): тогда пакеты получают ERROR, входящие не приходят. В REST ИШ такого признака нет,
// поэтому смотрим журнал ИШ (если он на этом сервере): итог последних опросов НРД
// «GetPackageListFull (GET) -> OK | #код». НРД не отвечает nsdAlertAfter — оповещение; снова
// отвечает — оповещение с простоем и числом недоставленных переводов (их нужно отправить повторно).
import (
"fmt"
"io"
"log"
"os"
"regexp"
"strings"
"sync"
"time"
)
const (
nsdAlertAfter = 10 * time.Minute
nsdRemindEvery = 2 * time.Hour
nsdLogTail = 512 << 10
)
var igateLogFile = "/var/lib/igate/.local/share/NSD Integration Gateway/logs-svc/log.txt"
// PID_363248 2026-09-25 15:42:59.1873 T16 DEBUG [WSL] WSL TEST3 GOST: GetPackageListFull (GET) -> #404 (HTTP Error 404: NotFound)
// PID_36882 2026-10-07 00:02:13.4942 T16 … — ИШ выравнивает номер процесса пробелами: с коротким номером (после
// перезапуска сервера) пробелов больше одного, и прежний шаблон с одним пробелом не находил ни одного опроса
// («НРД: нет данных опроса», серый индикатор при работающем обмене).
var nsdPollRe = regexp.MustCompile(`^PID_\d+\s+(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d)\.\d+ .*WSL ([^:]+): GetPackageListFull \(GET\) -> (OK|#\d+[^\r\n]*)`)
type nsdPoll struct {
At time.Time
Contour string
OK bool
Result string
}
// nsdLastPolls — итоги опросов НРД из хвоста журнала ИШ: последний опрос и первый неудачный после
// последнего успешного (начало текущего сбоя; nil — сбоя нет).
func nsdLastPolls(tail []byte) (last, firstFail *nsdPoll) {
return nsdLastPollsFor(tail, "")
}
// nsdLastPollsFor — то же для канала одного контура: при двух каналах в ИШ (TEST3 и боевой) опросы чужого
// контура не учитываются. prefix — начало имени канала в журнале («TEST3», «PROD»); пусто — все.
func nsdLastPollsFor(tail []byte, prefix string) (last, firstFail *nsdPoll) {
for _, ln := range strings.Split(string(tail), "\n") {
m := nsdPollRe.FindStringSubmatch(strings.TrimRight(ln, "\r"))
if m == nil || (prefix != "" && !strings.HasPrefix(strings.ToUpper(strings.TrimSpace(m[2])), prefix)) {
continue
}
at, err := time.ParseInLocation("2006-01-02 15:04:05", m[1], time.Local)
if err != nil {
continue
}
p := &nsdPoll{At: at, Contour: m[2], OK: m[3] == "OK", Result: m[3]}
last = p
if p.OK {
firstFail = nil
} else if firstFail == nil {
firstFail = p
}
}
return last, firstFail
}
func readTail(path string, n int64) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
st, err := f.Stat()
if err != nil {
return nil, err
}
off := st.Size() - n
if off < 0 {
off = 0
}
if _, err := f.Seek(off, io.SeekStart); err != nil {
return nil, err
}
return io.ReadAll(f)
}
var nsdWatch struct {
sync.Mutex
DownSince time.Time // первый неудачный опрос после последнего успешного
LastErr string
Contour string
Alerted bool
LastRemind time.Time
LastPollAt time.Time // последний опрос НРД шлюзом (по журналу)
LastPollOK time.Time // последний успешный
}
// nsdLinkStatus — для страницы статуса: не отвечает ли НРД шлюзу.
func nsdLinkStatus() (down bool, detail string) {
nsdWatch.Lock()
defer nsdWatch.Unlock()
if nsdWatch.DownSince.IsZero() {
return false, ""
}
return true, fmt.Sprintf("НРД (%s) не отвечает шлюзу с %s: %s", nsdWatch.Contour, nsdWatch.DownSince.Format("02.01 15:04"), nsdWatch.LastErr)
}
// nsdLinkTick — вызывается сторожем ИШ, когда сам ИШ отвечает и он на этом сервере.
func nsdLinkTick(rc *RuntimeConfig, now time.Time) {
tail, err := readTail(igateLogFile, nsdLogTail)
if err != nil {
return
}
last, firstFail := nsdLastPollsFor(tail, contourChannel(contourOf(rc.Snapshot().NSD.Profile)))
if last == nil {
return
}
w := &nsdWatch
w.Lock()
w.LastPollAt = last.At
if last.OK {
w.LastPollOK = last.At
} else if firstFail != nil && w.LastPollOK.IsZero() {
// в хвосте журнала нет успешного опроса после сбоя — ищем последний успешный до него
for _, ln := range strings.Split(string(tail), "\n") {
if m := nsdPollRe.FindStringSubmatch(strings.TrimRight(ln, "\r")); m != nil && m[3] == "OK" {
if at, err := time.ParseInLocation("2006-01-02 15:04:05", m[1], time.Local); err == nil {
w.LastPollOK = at
}
}
}
}
if last.OK {
since, alerted := w.DownSince, w.Alerted
w.DownSince, w.LastErr, w.Alerted = time.Time{}, "", false
w.Unlock()
if !since.IsZero() {
log.Printf("nsd-watch: НРД (%s) снова отвечает шлюзу, простой %s", last.Contour, humanDur(last.At.Sub(since)))
nsdProbeNow() // сквозную проверку — сразу, не дожидаясь планового срока
}
if alerted {
body := fmt.Sprintf("Контур НРД %s снова отвечает интеграционному шлюзу (простой %s, с %s).", last.Contour, humanDur(last.At.Sub(since)), since.Format("02.01 15:04"))
if n := sendFailCount(); n > 0 {
body += fmt.Sprintf(" Недоставленных переводов: %d — отправьте их повторно из карточки заявки («Отправить повторно»).", n)
}
raiseSystemAlert(rc, "НРД снова на связи", body)
}
return
}
if w.DownSince.IsZero() {
w.DownSince = firstFail.At
log.Printf("nsd-watch: НРД (%s) не отвечает шлюзу: %s", last.Contour, last.Result)
}
w.LastErr, w.Contour = last.Result, last.Contour
down := now.Sub(w.DownSince)
alert := !w.Alerted && down >= nsdAlertAfter
remind := w.Alerted && now.Sub(w.LastRemind) >= nsdRemindEvery
if alert || remind {
w.Alerted, w.LastRemind = true, now
}
since, lastErr := w.DownSince, w.LastErr
w.Unlock()
if !alert && !remind {
return
}
title := "НРД не отвечает шлюзу"
if remind {
title = "НРД всё ещё не отвечает шлюзу"
}
raiseSystemAlert(rc, title, fmt.Sprintf("Интеграционный шлюз работает, но контур НРД %s не отвечает %s (с %s): %s. Пока так, отправки в НРД не доходят (заявка остаётся «Не отправлена»), входящие не поступают. Это сторона НРД — при затяжном сбое обратитесь в поддержку НРД. Когда связь восстановится, bj сообщит.",
last.Contour, humanDur(down), since.Format("02.01 15:04"), lastErr))
}
func sendFailCount() int {
sendFails.Lock()
defer sendFails.Unlock()
sendFailLoad()
return len(sendFails.m)
}