package lkgateway // Версии систем, с которыми работает bj: собственные компоненты (ядро и модули) и внешние системы // (ИШ НРД, МНП QUIK, экспортёр ARQA, DEPO, СКЗИ «Валидата», криптосервис, PostgreSQL, Java, ОС). // Показываются общим списком в «Настройка → Обзор» и строкой в разделе каждого модуля. // Опрос баз и служб идёт с кэшем и в фоне, страница настроек не ждёт медленных источников. import ( "context" "encoding/json" "fmt" "io" "net/http" "os" "os/exec" "regexp" "strings" "sync" "time" "github.com/jackc/pgx/v5" "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/cryptocli" "git.zetit.ru/zuevav/Bridge-and-Join-s/internal/release" ) // SystemVersion — версия одной системы. type SystemVersion struct { Key string // bj-server, bj-npr, quik-mnp, depo, … Title string // пусто — система на этом сервере не используется Version string // пусто — не удалось определить Detail string // дата установки, сервер и т. п. Section string // раздел настроек (якорь без «sec-») Err string } // SystemVersions — сводка версий на момент At. type SystemVersions struct { Items []SystemVersion At time.Time } // Get — версия по ключу (пустая, если система не используется). func (v SystemVersions) Get(key string) SystemVersion { for _, x := range v.Items { if x.Key == key { return x } } return SystemVersion{Key: key} } const versionsTTL = 10 * time.Minute var versionsCache struct { sync.Mutex v SystemVersions refreshing bool } // systemVersions — версии из кэша. Первый раз собираются сразу, устаревшие обновляются в фоне. func systemVersions(rc *RuntimeConfig) SystemVersions { versionsCache.Lock() v := versionsCache.v if v.At.IsZero() { versionsCache.Unlock() return refreshVersions(rc) } if time.Since(v.At) > versionsTTL && !versionsCache.refreshing { versionsCache.refreshing = true go refreshVersions(rc) } versionsCache.Unlock() return v } // refreshVersions — опросить все источники (параллельно, у каждого свой тайм-аут). func refreshVersions(rc *RuntimeConfig) SystemVersions { probes := versionProbes(rc) items := make([]SystemVersion, len(probes)) var wg sync.WaitGroup for i, p := range probes { items[i] = SystemVersion{Key: p.key, Title: p.title, Section: p.section} if p.when != nil && !p.when() { items[i].Title = "" continue } wg.Add(1) go func(i int, p versionProbe) { defer wg.Done() ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() ver, detail, err := p.fn(ctx) items[i].Version, items[i].Detail = strings.TrimSpace(ver), strings.TrimSpace(detail) if err != nil { items[i].Err = err.Error() } }(i, p) } wg.Wait() out := SystemVersions{At: time.Now()} for _, it := range items { if it.Title != "" { out.Items = append(out.Items, it) } } versionsCache.Lock() versionsCache.v, versionsCache.refreshing = out, false versionsCache.Unlock() licenseVersionWatch(rc, out) // смена версии внешней системы при закрытых обновлениях bj return out } type versionProbe struct { key, title, section string when func() bool fn func(ctx context.Context) (version, detail string, err error) } func fileExists(p string) bool { _, err := os.Stat(p) return err == nil } func versionProbes(rc *RuntimeConfig) []versionProbe { s := rc.Snapshot() comp := func(name string) func() bool { return func() bool { c, ok := release.ComponentByName(name) return ok && fileExists(c.Path) } } return []versionProbe{ {key: "bj-server", title: "Ядро bj (bj-server)", section: "update", fn: func(context.Context) (string, string, error) { return BuildVersion, "", nil }}, {key: "bj-component-install", title: "Установщик обновлений bj", section: "update", when: comp("bj-component-install"), fn: func(ctx context.Context) (string, string, error) { return componentVersion(ctx, "bj-component-install") }}, {key: "bj-npr", title: "Модуль «НПР / УДС» (bj-npr)", section: "quikexport", when: comp("bj-npr"), fn: func(ctx context.Context) (string, string, error) { return componentVersion(ctx, "bj-npr") }}, {key: "bj-quikexport", title: "Модуль «Экспорт QUIK» (bj-quikexport)", section: "quikexport", when: comp("bj-quikexport"), fn: func(ctx context.Context) (string, string, error) { if m, _, err := quikExportVersions(ctx, rc); err == nil && m != "" { return m, "", nil } return componentVersion(ctx, "bj-quikexport") }}, {key: "quikexport", title: "Экспортёр QUIK (ARQA quikexport)", section: "quikexport", when: comp("bj-quikexport"), fn: func(ctx context.Context) (string, string, error) { _, e, err := quikExportVersions(ctx, rc) return e, "", err }}, {key: "bj-lk", title: "Личный кабинет клиента (bj-lk)", section: "lk", when: comp("bj-lk"), fn: func(ctx context.Context) (string, string, error) { return componentVersion(ctx, "bj-lk") }}, {key: "crypto-service.jar", title: "Проверка ключа подписи (bj-crypto)", section: "crypto", when: func() bool { return s.Crypto.Provider != "" && s.Crypto.Provider != string(cryptocli.ProviderStub) }, fn: func(ctx context.Context) (string, string, error) { cli := cryptocli.New(cryptocli.Config{Provider: cryptocli.Provider(s.Crypto.Provider), SocketPath: s.Crypto.SocketPath, ModulePath: s.Crypto.ModulePath}) defer cli.Close() h, err := cli.Health(ctx) if err != nil { return "", "", err } ver, _, _ := strings.Cut(h.Message, " (") return ver, "провайдер " + h.Provider, nil }}, {key: "validata", title: "СКЗИ «Валидата Клиент L» (версия производителя, как на moex.com)", section: "crypto", when: func() bool { return fileExists(validataUtil) }, fn: validataVersion}, {key: "java", title: "Java (для проверки ключа)", section: "crypto", when: func() bool { _, err := exec.LookPath("java"); return err == nil }, fn: javaVersion}, {key: "ish", title: "Интеграционный шлюз НРД (ИШ)", section: "nsd", when: func() bool { return fileExists(ishDepsPath) }, fn: func(ctx context.Context) (string, string, error) { info := ishVersionInfo(ctx) return info.Installed, info.Note, nil }}, {key: "quik-mnp", title: "QUIK — Модуль неторговых поручений", section: "quik", when: func() bool { return s.Quik.Server != "" }, fn: func(ctx context.Context) (string, string, error) { return quikMNPVersion(ctx, s.Quik) }}, {key: "depo", title: "Fancy DEPO (база DEPO)", section: "fancy", when: func() bool { return s.Fancy.Server != "" }, fn: func(ctx context.Context) (string, string, error) { return depoVersion(ctx, s.Fancy) }}, {key: "postgres", title: "PostgreSQL (база bj)", section: "db", when: func() bool { return s.Postgres.DSN != "" }, fn: func(ctx context.Context) (string, string, error) { return postgresVersion(ctx, s.Postgres.DSN) }}, {key: "os", title: "Операционная система", section: "overview", fn: osVersion}, } } // componentVersion — версия компонента bj: запись установщика обновлений, иначе «<файл> -version». func componentVersion(ctx context.Context, name string) (string, string, error) { installed := release.LoadInstalled(release.InstalledFile) for _, st := range release.Plan("", nil, installed, BuildVersion) { // Только запись установщика с совпавшим файлом: для ядра без записи Plan подставляет версию bj-server. if st.Name == name && st.Installed != "" && st.Installed == installed[name].Version { return st.Installed, "", nil } } c, _ := release.ComponentByName(name) out, err := exec.CommandContext(ctx, c.Path, "-version").Output() if err != nil { return "", "", nil // старые сборки без -version — версия неизвестна, это не ошибка } f := strings.Fields(string(out)) if len(f) >= 2 && f[0] == name { return f[1], "", nil } return "", "", nil } // quikExportVersions — версии модуля и экспортёра ARQA от службы bj-quikexport. func quikExportVersions(ctx context.Context, rc *RuntimeConfig) (module, exporter string, err error) { resp, err := quikExportDo(ctx, rc, http.MethodGet, "/version", nil, "") if err != nil { return "", "", fmt.Errorf("служба bj-quikexport не отвечает") } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", "", fmt.Errorf("служба bj-quikexport ответила %d", resp.StatusCode) } var v struct { Module string `json:"module"` Exporter string `json:"exporter"` } if err := json.NewDecoder(io.LimitReader(resp.Body, 1<<16)).Decode(&v); err != nil { return "", "", err } return v.Module, v.Exporter, nil } const validataUtil = "/opt/Validata/VDCSP/bin/amd64/zpki1utl" var validataVerRe = regexp.MustCompile(`версия\s+(\d+(?:\.\d+)+)`) func validataVersion(ctx context.Context) (string, string, error) { out, _ := exec.CommandContext(ctx, validataUtil, "-help").CombinedOutput() // справка завершается с ненулевым кодом if m := validataVerRe.FindSubmatch(out); m != nil { return string(m[1]), "/opt/Validata/VDCSP", nil } return "", "", fmt.Errorf("версия не найдена в справке zpki1utl") } var javaVerRe = regexp.MustCompile(`version "([^"]+)"`) func javaVersion(ctx context.Context) (string, string, error) { out, err := exec.CommandContext(ctx, "java", "-version").CombinedOutput() if m := javaVerRe.FindSubmatch(out); m != nil { first, _, _ := strings.Cut(string(out), " version") return string(m[1]), first, nil } if err != nil { return "", "", err } return "", "", fmt.Errorf("версия не распознана") } // quikMNPVersion — последняя запись VersionInfo в БД МНП (история обновлений ARQA). func quikMNPVersion(ctx context.Context, q QuikMNPSettings) (string, string, error) { conn, err := quikOpen(q) if err != nil { return "", "", err } defer conn.Close() var maj, min, patch int64 var at *time.Time if err := conn.QueryRowContext(ctx, "SELECT TOP 1 MajVersion, MinVersion, Patch, UPDATETime FROM VersionInfo ORDER BY MajVersion DESC, MinVersion DESC, Patch DESC"). Scan(&maj, &min, &patch, &at); err != nil { return "", "", fmt.Errorf("VersionInfo: %v", err) } detail := "" if at != nil { detail = "обновлена " + at.Format("02.01.2006") } return fmt.Sprintf("%d.%d.%d", maj, min, patch), detail, nil } // depoVersion — последняя версия данных DEPO (OD_DATA_VERSION). func depoVersion(ctx context.Context, f FancyMSSQLSettings) (string, string, error) { db := f.DepoDB() conn, err := fancyOpen(f, db) if err != nil { return "", "", err } defer conn.Close() q := "SELECT TOP 1 DATA_VERSION, V_DATE FROM OD_DATA_VERSION ORDER BY V_DATE DESC" if f.IsPostgres() { q = "SELECT data_version, v_date FROM od_data_version ORDER BY v_date DESC LIMIT 1" } var ver string var at *time.Time if err := conn.QueryRowContext(ctx, q).Scan(&ver, &at); err != nil { return "", "", fmt.Errorf("OD_DATA_VERSION: %v", err) } detail := "база " + db if at != nil { detail = "обновлена " + at.Format("02.01.2006") + " · " + detail } return ver, detail, nil } func postgresVersion(ctx context.Context, dsn string) (string, string, error) { conn, err := pgx.Connect(ctx, dsn) if err != nil { return "", "", err } defer conn.Close(context.Background()) var v string if err := conn.QueryRow(ctx, "SHOW server_version").Scan(&v); err != nil { return "", "", err } ver, rest, _ := strings.Cut(v, " ") return ver, strings.Trim(rest, "()"), nil } func osVersion(context.Context) (string, string, error) { if b, err := os.ReadFile("/etc/astra_version"); err == nil { return "Astra Linux " + strings.TrimSpace(string(b)), "", nil } b, err := os.ReadFile("/etc/os-release") if err != nil { return "", "", err } for _, ln := range strings.Split(string(b), "\n") { if v, ok := strings.CutPrefix(ln, "PRETTY_NAME="); ok { return strings.Trim(v, `"`), "", nil } } return "", "", nil } // refreshVersionsHandler — POST /admin/setup/versions/refresh. func (h *setupHandlers) refreshVersionsHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method", http.StatusMethodNotAllowed) return } v := refreshVersions(h.rc) known := 0 for _, it := range v.Items { if it.Version != "" { known++ } } setupFlash(w, r, fmt.Sprintf("Версии систем обновлены: определено %d из %d.", known, len(v.Items))) }