Версии систем: общий список в «Обзоре» и строка в разделе каждого модуля
bj собирает версии своих компонентов и внешних систем (в фоне, кэш 10 минут, прогрев при старте): ядро и модули bj (components.json или «-version»), экспортёр ARQA (служба bj-quikexport, /version), криптосервис (Health), «Валидата CSP» (zpki1utl), Java, ИШ НРД (deps.json + сайт НРД), МНП QUIK (VersionInfo), DEPO (OD_DATA_VERSION), PostgreSQL, ОС. «Настройка → Обзор» — таблица «Версии систем» с кнопкой «Обновить сведения»; в разделах БД, Fancy, QUIK МНП, Экспорт QUIK, Криптография, НРД, ЛК, НПР и Обновления — строка с версией. В таблице обновлений у компонентов, выложенных вручную, вместо «неизвестна» — версия самого компонента. bj-component-install: -version. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -22,12 +22,20 @@ import (
|
||||
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/release"
|
||||
)
|
||||
|
||||
// version — версия установщика (go build -ldflags "-X main.version=…").
|
||||
var version = "0.1.0"
|
||||
|
||||
func main() {
|
||||
fs := flag.NewFlagSet("bj-component-install", flag.ExitOnError)
|
||||
root := fs.String("root", "", "корень установки (для проверки в песочнице; по умолчанию /)")
|
||||
pubPath := fs.String("pub", "/etc/bj/update.pub", "открытый ключ подписи релизов (base64 Ed25519), файл root")
|
||||
noRestart := fs.Bool("no-restart", false, "не перезапускать службы")
|
||||
showVersion := fs.Bool("version", false, "показать версию")
|
||||
_ = fs.Parse(os.Args[1:])
|
||||
if *showVersion {
|
||||
fmt.Println("bj-component-install", version)
|
||||
return
|
||||
}
|
||||
var res release.InstallResult
|
||||
if pub, err := release.LoadPublicKey(*pubPath); err != nil {
|
||||
res.Error = fmt.Sprintf("открытый ключ обновлений (%s): %v", *pubPath, err)
|
||||
|
||||
@@ -35,7 +35,7 @@ func main() {
|
||||
fmt.Println("bj-quikexport", version)
|
||||
return
|
||||
}
|
||||
srv, err := quikexport.New(quikexport.Config{StorePath: *store, SetupPath: *setup, KeyPath: *key})
|
||||
srv, err := quikexport.New(quikexport.Config{StorePath: *store, SetupPath: *setup, KeyPath: *key, Version: version})
|
||||
if err != nil {
|
||||
log.Fatalf("bj-quikexport: %v", err)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Страница службы модуля встраивается в макет bj: меню и заголовок bj, содержимое — от модуля.
|
||||
@@ -52,3 +53,26 @@ func TestSetupPageEmbedsQuikExport(t *testing.T) {
|
||||
t.Error("индикатор меню не взят у службы")
|
||||
}
|
||||
}
|
||||
|
||||
// Версии: общий список в «Обзоре» и строка в разделе модуля.
|
||||
func TestSetupPageShowsVersions(t *testing.T) {
|
||||
a, err := newAdmin()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
vers := SystemVersions{At: time.Now(), Items: []SystemVersion{
|
||||
{Key: "quik-mnp", Title: "QUIK — Модуль неторговых поручений", Version: "21.3.0", Detail: "обновлена 05.06.2026", Section: "quik"},
|
||||
{Key: "depo", Title: "Fancy DEPO (база DEPO)", Section: "fancy", Err: "нет связи"},
|
||||
}}
|
||||
rec := httptest.NewRecorder()
|
||||
render(rec, a.setup, SetupData{page: nowPage("Настройка", "setup"), AdvancedUnlocked: true, Modules: map[string]bool{"quik-mnp": true}, Versions: vers})
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("код %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
body := rec.Body.String()
|
||||
for _, want := range []string{"Версии систем", "<strong>21.3.0</strong>", "QUIK — Модуль неторговых поручений: <strong>21.3.0</strong> · обновлена 05.06.2026", "не определена", "нет связи"} {
|
||||
if !strings.Contains(body, want) {
|
||||
t.Errorf("в странице нет %q", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,6 +303,7 @@ func (s *Server) Run(ctx context.Context) error {
|
||||
|
||||
// Фоновая авто-проверка обновлений из артефактории (если включена).
|
||||
go NewUpdater(s.rc).updateLoop(ctx)
|
||||
go refreshVersions(s.rc) // версии систем для «Настройки» собираются заранее
|
||||
|
||||
// Лицензия: ежесуточное продление ключа на сервере лицензий lic.zetit.ru.
|
||||
go superviseLoop(ctx, "license", s.rc, s.licenseLoop)
|
||||
|
||||
@@ -105,6 +105,7 @@ func registerSetup(mux *http.ServeMux, a *admin, rc *RuntimeConfig, svc *Service
|
||||
|
||||
// Доверенные сертификаты НРД в справочнике ИШ (вшитый комплект + ручные).
|
||||
registerQuikExportSetup(mux, h)
|
||||
mux.HandleFunc("/admin/setup/versions/refresh", h.refreshVersionsHandler)
|
||||
registerUsersSetup(mux, h)
|
||||
mux.HandleFunc("/admin/setup/npr", h.saveNPR)
|
||||
mux.HandleFunc("/admin/setup/npr/test", h.testNPR)
|
||||
@@ -304,6 +305,7 @@ type SetupData struct {
|
||||
DepoJob DepoJobView
|
||||
DepoTables DepoTablesView // трансферные таблицы FTV_M2M* в базе DEPO
|
||||
QuikExport QuikExportView // модуль «Экспорт биржевой информации»
|
||||
Versions SystemVersions // версии компонентов bj и внешних систем
|
||||
Modules map[string]bool // какие лицензируемые модули разрешены (ключ = признак)
|
||||
Features []LicenseFeatureView // признаки лицензии для карточки
|
||||
Users UsersView // пользователи, роли, сессии
|
||||
@@ -361,6 +363,7 @@ func (h *setupHandlers) renderSetup(w http.ResponseWriter, req *http.Request, fl
|
||||
DepoJob: depoJobView(s.Fancy),
|
||||
DepoTables: depoTablesView(s.Fancy),
|
||||
QuikExport: quikExportView(h.rc, req.URL.Query().Get("reveal_ro") == "1"),
|
||||
Versions: systemVersions(h.rc),
|
||||
Modules: modulesAllowed(h.rc),
|
||||
Features: licenseFeatureViews(h.rc),
|
||||
Users: usersView(w),
|
||||
|
||||
@@ -286,7 +286,11 @@ func updateComponentRows(rc *RuntimeConfig) ([]release.ComponentState, bool) {
|
||||
avail := rc.Snapshot().Update.Components
|
||||
var out []release.ComponentState
|
||||
any := false
|
||||
vers := systemVersions(rc)
|
||||
for _, r := range rows {
|
||||
if r.Present && r.Installed == "" {
|
||||
r.Installed = vers.Get(r.Name).Version // выложен вручную — версия со слов самого компонента
|
||||
}
|
||||
r.Available = avail[r.Name]
|
||||
r.Update = r.Available != "" && (r.Present || r.Core) && (r.Installed == "" || release.IsNewer(r.Available, r.Installed))
|
||||
if r.Present || r.Core || r.Available != "" {
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
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()
|
||||
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: "npr", 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: "Криптосервис (crypto-service)", 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: "СКЗИ «Валидата CSP»", 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)))
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
//go:build live
|
||||
|
||||
package lkgateway
|
||||
|
||||
// Живой опрос версий систем на этом сервере.
|
||||
// go test -c -tags live -o versions.test ./internal/lkgateway/ ; запуск от bj: -test.run TestVersionsLive -test.v
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestVersionsLive(t *testing.T) {
|
||||
rc, err := NewRuntimeConfig("/var/lib/bj/.bj/setup.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, it := range refreshVersions(rc).Items {
|
||||
t.Logf("%-40s %-14s %s %s", it.Title, it.Version, it.Detail, it.Err)
|
||||
}
|
||||
}
|
||||
@@ -37,11 +37,25 @@
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card" id="versions">
|
||||
<h2>Версии систем</h2>
|
||||
<p class="muted">Что установлено на этом сервере и с чем работает bj: собственные компоненты и внешние системы. Та же версия показана в разделе каждого модуля. Сведения на {{msk .Versions.At}}.</p>
|
||||
<div style="overflow-x:auto"><table style="margin-top:8px">
|
||||
<thead><tr><th>Система</th><th>Версия</th><th>Подробности</th></tr></thead>
|
||||
<tbody>{{range .Versions.Items}}<tr>
|
||||
<td>{{if and .Section (ne .Section "overview")}}<a href="#{{.Section}}" onclick="var b=document.querySelector('.settings-nav [data-sec="{{.Section}}"]');if(b){b.click();return false}">{{.Title}}</a>{{else}}{{.Title}}{{end}}</td>
|
||||
<td>{{if .Version}}<strong>{{.Version}}</strong>{{else}}<span class="muted">не определена</span>{{end}}</td>
|
||||
<td class="muted" style="font-size:12.5px">{{.Detail}}{{with .Err}} <span style="color:var(--err)">{{.}}</span>{{end}}</td>
|
||||
</tr>{{end}}</tbody>
|
||||
</table></div>
|
||||
<form method="post" action="/admin/setup/versions/refresh" style="margin-top:10px"><input type="hidden" name="section" value="versions"><button type="submit" class="btn btn-secondary">Обновить сведения</button></form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{/* ============ БАЗА ДАННЫХ ============ */}}
|
||||
<section class="settings-section" id="sec-db">
|
||||
<h1>База данных</h1>
|
||||
{{template "verline" (.Versions.Get "postgres")}}
|
||||
<div class="card">
|
||||
<h2><span class="dot {{if .Settings.Postgres.DSN}}ok{{else}}err{{end}}"></span>PostgreSQL</h2>
|
||||
<p class="muted">Журнал сделок m2m-core и принимающая БД. Сейчас: {{if .Settings.Postgres.DSN}}<code>подключено</code>{{else}}<code>in-memory</code> (данные не сохраняются){{end}}.</p>
|
||||
@@ -70,6 +84,7 @@
|
||||
{{/* ============ ОБМЕН FANCY (MSSQL) — перспективный модуль ============ */}}
|
||||
<section class="settings-section" id="sec-fancy">
|
||||
<h1>Обмен Fancy (MSSQL / PostgreSQL)</h1>
|
||||
{{template "verline" (.Versions.Get "depo")}}
|
||||
<div class="card" id="sec-fancy-conn">
|
||||
<h2><span class="dot {{if .Settings.Fancy.DepoTablesCheckOK}}ok{{else if .Settings.Fancy.Server}}warn{{else}}err{{end}}"></span>Подключение к серверу БД клиента</h2>
|
||||
<p class="muted">Сервер учётной базы Fancy DEPO: картотека лиц, счета и договоры, остатки, опции (признак квалифицированного инвестора), трансферные таблицы обмена. Поддерживаются <strong>Microsoft SQL Server</strong> и <strong>PostgreSQL</strong>. База указывается ниже, в карточке «Трансферные таблицы DEPO».</p>
|
||||
@@ -177,6 +192,7 @@
|
||||
{{/* ============ QUIK МНП ============ */}}
|
||||
<section class="settings-section" id="sec-quik">
|
||||
<h1>QUIK — Модуль неторговых поручений</h1>
|
||||
{{template "verline" (.Versions.Get "quik-mnp")}}
|
||||
<div class="card">
|
||||
<h2><span class="dot {{if .Settings.Quik.Verified}}ok{{else if .Settings.Quik.Server}}warn{{else}}err{{end}}"></span>Подключение к БД МНП QUIK (MSSQL)</h2>
|
||||
<p class="muted">При выводе бумаг клиентом bj блокирует их количество в QUIK через API Модуля неторговых поручений (хранимые процедуры). БД <strong>боевая</strong> — тест только на чтение (версия + наличие M2M-объектов), без записи. Подключение под логином с ролью <code>InstrEu</code>.<br>
|
||||
@@ -368,6 +384,7 @@
|
||||
{{/* ============ КРИПТОГРАФИЯ ============ */}}
|
||||
<section class="settings-section" id="sec-crypto">
|
||||
<h1>Криптография</h1>
|
||||
{{template "verline" (.Versions.Get "crypto-service.jar")}}{{template "verline" (.Versions.Get "validata")}}{{template "verline" (.Versions.Get "java")}}
|
||||
|
||||
<div class="card" style="background:var(--accent-weak)">
|
||||
<h2>Установка ключа на флешку</h2>
|
||||
@@ -451,6 +468,7 @@
|
||||
{{/* ============ НРД ============ */}}
|
||||
<section class="settings-section" id="sec-nsd">
|
||||
<h1>НРД</h1>
|
||||
{{template "verline" (.Versions.Get "ish")}}
|
||||
<div class="card">
|
||||
<h2><span class="dot {{if .Settings.NSD.IGWBaseURL}}ok{{else}}warn{{end}}"></span>Интеграционный шлюз (ИШ)</h2>
|
||||
<p class="muted">{{if not .Settings.NSD.IGWBaseURL}}<code>mock-режим</code> — без реального ИШ.{{else}}Профиль <code>{{.Settings.NSD.Profile}}</code>, ИШ <code>{{.Settings.NSD.IGWBaseURL}}</code>.{{end}} Стенды и установка — <a href="/admin/help/systems">справка</a>.</p>
|
||||
@@ -508,6 +526,7 @@
|
||||
<section class="settings-section" id="sec-lk">
|
||||
<h1>Личный кабинет <span class="muted" style="font-size:16px;font-weight:500">· ЕСИА Финанс</span></h1>
|
||||
<p class="muted" style="margin:-8px 0 16px">Интеграция с платформой ЛК <strong>ЕСИА Финанс</strong>: адрес для клиента, приём заявок M2M, возврат статуса и подключение к API (тест/бой). НРД настраивается отдельно — в разделе «НРД».</p>
|
||||
{{template "verline" (.Versions.Get "bj-lk")}}
|
||||
|
||||
<div class="card">
|
||||
<h2><span class="dot {{if .Settings.LK.PublicURL}}ok{{else}}warn{{end}}"></span>Личный кабинет — интеграция</h2>
|
||||
@@ -792,6 +811,7 @@
|
||||
{{/* ============ ОБНОВЛЕНИЯ ============ */}}
|
||||
<section class="settings-section" id="sec-update">
|
||||
<h1>Обновления</h1>
|
||||
{{template "verline" (.Versions.Get "bj-server")}}
|
||||
|
||||
<div class="card">
|
||||
<h2>Компоненты</h2>
|
||||
@@ -841,6 +861,7 @@
|
||||
<div class="card" id="sec-npr">
|
||||
<h2><span class="dot {{if .Settings.NPR.Enabled}}ok{{else}}warn{{end}}"></span>Модуль «НПР / УДС» (риск-монитор по данным QUIK)</h2>
|
||||
<p class="muted">Показывает НПР1 / НПР2 / УДС клиентов маржинальной торговли по базе экспорта QUIK (схема <code>npr</code>). Отдельно лицензируемый модуль (признак <code>npr</code>); при включении в меню появляется пункт «НПР / УДС». Подключение — логин только для чтения.</p>
|
||||
{{template "verline" (.Versions.Get "bj-npr")}}
|
||||
<form method="post" action="/admin/setup/npr" style="margin-top:10px;display:grid;gap:10px;max-width:640px">
|
||||
<input type="hidden" name="section" value="sec-npr">
|
||||
<label style="display:flex;gap:8px;align-items:center;font-weight:600"><input type="checkbox" name="enabled" value="1" {{if .Settings.NPR.Enabled}}checked{{end}}> Включить модуль</label>
|
||||
@@ -991,3 +1012,4 @@
|
||||
})();
|
||||
</script>
|
||||
{{end}}
|
||||
{{define "verline"}}{{if .Title}}<p class="muted" style="font-size:12.5px;margin:-6px 0 12px">{{.Title}}: {{if .Version}}<strong>{{.Version}}</strong>{{else}}не определена{{end}}{{with .Detail}} · {{.}}{{end}}{{with .Err}} · <span style="color:var(--err)">{{.}}</span>{{end}}</p>{{end}}{{end}}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package quikexport
|
||||
|
||||
// Версия экспортёра ARQA: строка вида 12.1.5.2 внутри программы export. Кандидат сверяется с версией
|
||||
// установщика из .actions.log (install_quikexport-12.1.5.run), чтобы не принять чужую версию библиотеки.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
exporterVerRe = regexp.MustCompile(`^\d{1,3}\.\d+\.\d+\.\d+$`)
|
||||
exporterInstallerRe = regexp.MustCompile(`install_quikexport-(\d+\.\d+\.\d+)\.run`)
|
||||
)
|
||||
|
||||
var exporterVerCache struct {
|
||||
sync.Mutex
|
||||
path string
|
||||
mod time.Time
|
||||
ver string
|
||||
}
|
||||
|
||||
// exporterVersion — версия ARQA quikexport в каталоге dir ("" — не определена).
|
||||
func exporterVersion(dir string) string {
|
||||
bin := filepath.Join(dir, "export")
|
||||
fi, err := os.Stat(bin)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
exporterVerCache.Lock()
|
||||
defer exporterVerCache.Unlock()
|
||||
if exporterVerCache.path == bin && exporterVerCache.mod.Equal(fi.ModTime()) {
|
||||
return exporterVerCache.ver
|
||||
}
|
||||
installer := ""
|
||||
if log, err := os.ReadFile(filepath.Join(dir, ".actions.log")); err == nil {
|
||||
if m := exporterInstallerRe.FindAllSubmatch(log, -1); len(m) > 0 {
|
||||
installer = string(m[len(m)-1][1])
|
||||
}
|
||||
}
|
||||
ver := ""
|
||||
if b, err := os.ReadFile(bin); err == nil {
|
||||
for _, tok := range bytes.Split(b, []byte{0}) {
|
||||
if len(tok) > 20 || !exporterVerRe.Match(tok) {
|
||||
continue
|
||||
}
|
||||
if installer == "" || bytes.HasPrefix(tok, []byte(installer+".")) {
|
||||
ver = string(tok)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if ver == "" {
|
||||
ver = installer
|
||||
}
|
||||
exporterVerCache.path, exporterVerCache.mod, exporterVerCache.ver = bin, fi.ModTime(), ver
|
||||
return ver
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package quikexport
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExporterVersion(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
// В программе встречаются и чужие версии библиотек — берётся та, что совпадает с установщиком.
|
||||
bin := []byte("\x00ELF\x008.0.4.2\x00codecvt\x0012.1.5.2\x000123456789\x00")
|
||||
if err := os.WriteFile(filepath.Join(dir, "export"), bin, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, ".actions.log"), []byte("[14/09/26][./install_quikexport-12.1.5.run][root] Done\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v := exporterVersion(dir); v != "12.1.5.2" {
|
||||
t.Fatalf("версия экспортёра = %q", v)
|
||||
}
|
||||
if v := exporterVersion(t.TempDir()); v != "" {
|
||||
t.Fatalf("без экспортёра версия должна быть пустой, получено %q", v)
|
||||
}
|
||||
}
|
||||
@@ -856,6 +856,9 @@ type QuikExportView struct {
|
||||
ShowRO bool
|
||||
ServerIP string
|
||||
Warnings []string
|
||||
|
||||
ModuleVersion string // bj-quikexport
|
||||
ExporterVersion string // ARQA quikexport (\"\" — не определена)
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
@@ -7,7 +7,8 @@ package quikexport
|
||||
// в заголовке X-BJ-Indicator — цвет индикатора меню (ok | warn | err | пусто);
|
||||
// - POST /admin/setup/quikexport[/keys|apply|import|service|dbinit] — действие; итог — 204
|
||||
// с сообщением в X-BJ-Flash, bj-server показывает его как обычное сообщение страницы настроек;
|
||||
// - GET /admin/setup/quikexport/sql и /preview — файлы как есть.
|
||||
// - GET /admin/setup/quikexport/sql и /preview — файлы как есть;
|
||||
// - GET /version — версии модуля и экспортёра ARQA (JSON) для общего списка версий bj.
|
||||
//
|
||||
// Лицензию проверяет bj-server и передаёт в X-BJ-Licensed: 1.
|
||||
|
||||
@@ -38,6 +39,7 @@ type Config struct {
|
||||
StorePath string // собственные настройки модуля
|
||||
SetupPath string // setup.json bj: отсюда настройки переносятся при первом запуске (раздел quik_export)
|
||||
KeyPath string // служебный ключ модулей
|
||||
Version string // версия модуля bj-quikexport
|
||||
}
|
||||
|
||||
// Server — служба модуля.
|
||||
@@ -77,6 +79,7 @@ func (srv *Server) Handler() http.Handler {
|
||||
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("ok")) })
|
||||
mux.HandleFunc("/section", func(w http.ResponseWriter, r *http.Request) {
|
||||
v := quikExportView(srv.store, licensed(r), r.URL.Query().Get("reveal_ro") == "1")
|
||||
v.ModuleVersion, v.ExporterVersion = srv.cfg.Version, exporterVersion(v.S.Dir)
|
||||
var buf bytes.Buffer
|
||||
if err := srv.section.ExecuteTemplate(&buf, "section", v); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
@@ -87,6 +90,10 @@ func (srv *Server) Handler() http.Handler {
|
||||
w.Header().Set("X-BJ-Indicator", indicator(v))
|
||||
_, _ = w.Write(buf.Bytes())
|
||||
})
|
||||
mux.HandleFunc("/version", func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"module": srv.cfg.Version, "exporter": exporterVersion(srv.store.Get().Dir)})
|
||||
})
|
||||
mux.HandleFunc("/admin/setup/quikexport", srv.saveQuikExport)
|
||||
mux.HandleFunc("/admin/setup/quikexport/keys", srv.uploadQuikExportKeys)
|
||||
mux.HandleFunc("/admin/setup/quikexport/apply", srv.applyQuikExport)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{{$X := .}}{{$S := $X.S}}
|
||||
<h1>QUIK — Экспорт биржевой информации</h1>
|
||||
<p class="muted">Модуль ARQA «Экспорт биржевой информации» выгружает данные сервера QUIK (портфели клиентов, позиции, сделки, справочники) в базу PostgreSQL. bj управляет им отсюда: ключи и логин QUIK, где держим базу, какой объём данных выгружаем, состояние службы и место на диске. На этих данных работают модуль «НПР / УДС» и дополнительные проверки M2M. Лицензионный признак <code>quik-export</code>.</p>
|
||||
<p class="muted" style="font-size:12.5px;margin:-6px 0 12px">Экспортёр QUIK (ARQA quikexport): {{if $X.ExporterVersion}}<strong>{{$X.ExporterVersion}}</strong>{{else}}не определена{{end}} · модуль bj-quikexport{{with $X.ModuleVersion}} <strong>{{.}}</strong>{{end}}</p>
|
||||
{{range $X.Warnings}}<div class="card" style="padding:8px 14px;margin-bottom:8px;background:color-mix(in srgb,var(--warn) 10%,var(--card));display:flex;gap:8px;align-items:center"><span class="dot warn"></span>{{.}}</div>{{end}}
|
||||
|
||||
<div class="card">
|
||||
|
||||
@@ -23,7 +23,7 @@ ver() { eval "v=\${$1:-}"; [ -n "$v" ] && echo "$v" || echo "$VERSION"; }
|
||||
echo "Сборка релиза $VERSION (канал $CHANNEL) в $DIST"
|
||||
go build -trimpath -ldflags "-s -w -X $MOD/internal/lkgateway.BuildVersion=$(ver BJ_SERVER_VERSION)" -o "$DIST/bj-server" ./cmd/bj-server
|
||||
CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X main.installerVersion=$(ver INSTALLER_VERSION)" -o "$DIST/bj-installer" ./cmd/bj-installer
|
||||
CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o "$DIST/bj-component-install" ./cmd/bj-component-install
|
||||
CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X main.version=$(ver COMPONENT_INSTALL_VERSION)" -o "$DIST/bj-component-install" ./cmd/bj-component-install
|
||||
CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X main.version=$(ver NPR_VERSION)" -o "$DIST/bj-npr" ./cmd/bj-npr
|
||||
ver NPR_VERSION > "$DIST/bj-npr.version"
|
||||
CGO_ENABLED=0 go build -trimpath -ldflags "-s -w -X main.version=$(ver QUIKEXPORT_VERSION)" -o "$DIST/bj-quikexport" ./cmd/bj-quikexport
|
||||
|
||||
Reference in New Issue
Block a user