diff --git a/cmd/bj-component-install/main.go b/cmd/bj-component-install/main.go index 6d25aef..378b1cd 100644 --- a/cmd/bj-component-install/main.go +++ b/cmd/bj-component-install/main.go @@ -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) diff --git a/cmd/bj-quikexport/main.go b/cmd/bj-quikexport/main.go index 9580a85..fe634b6 100644 --- a/cmd/bj-quikexport/main.go +++ b/cmd/bj-quikexport/main.go @@ -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) } diff --git a/internal/lkgateway/npr_proxy_test.go b/internal/lkgateway/npr_proxy_test.go index 86d2344..1dfef00 100644 --- a/internal/lkgateway/npr_proxy_test.go +++ b/internal/lkgateway/npr_proxy_test.go @@ -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{"Версии систем", "21.3.0", "QUIK — Модуль неторговых поручений: 21.3.0 · обновлена 05.06.2026", "не определена", "нет связи"} { + if !strings.Contains(body, want) { + t.Errorf("в странице нет %q", want) + } + } +} diff --git a/internal/lkgateway/server.go b/internal/lkgateway/server.go index c9408a0..5a2f411 100644 --- a/internal/lkgateway/server.go +++ b/internal/lkgateway/server.go @@ -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) diff --git a/internal/lkgateway/setup.go b/internal/lkgateway/setup.go index 4106d07..7cfa8bc 100644 --- a/internal/lkgateway/setup.go +++ b/internal/lkgateway/setup.go @@ -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), diff --git a/internal/lkgateway/updater.go b/internal/lkgateway/updater.go index 80aba7e..e7c8150 100644 --- a/internal/lkgateway/updater.go +++ b/internal/lkgateway/updater.go @@ -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 != "" { diff --git a/internal/lkgateway/versions.go b/internal/lkgateway/versions.go new file mode 100644 index 0000000..c60be68 --- /dev/null +++ b/internal/lkgateway/versions.go @@ -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))) +} diff --git a/internal/lkgateway/versions_live_test.go b/internal/lkgateway/versions_live_test.go new file mode 100644 index 0000000..26a45b8 --- /dev/null +++ b/internal/lkgateway/versions_live_test.go @@ -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) + } +} diff --git a/internal/lkgateway/web/templates/admin_setup.html b/internal/lkgateway/web/templates/admin_setup.html index 272e5ae..aef7145 100644 --- a/internal/lkgateway/web/templates/admin_setup.html +++ b/internal/lkgateway/web/templates/admin_setup.html @@ -37,11 +37,25 @@ {{end}} +
+

Версии систем

+

Что установлено на этом сервере и с чем работает bj: собственные компоненты и внешние системы. Та же версия показана в разделе каждого модуля. Сведения на {{msk .Versions.At}}.

+
+ + {{range .Versions.Items}} + + + + {{end}} +
СистемаВерсияПодробности
{{if and .Section (ne .Section "overview")}}{{.Title}}{{else}}{{.Title}}{{end}}{{if .Version}}{{.Version}}{{else}}не определена{{end}}{{.Detail}}{{with .Err}} {{.}}{{end}}
+
+
{{/* ============ БАЗА ДАННЫХ ============ */}}

База данных

+ {{template "verline" (.Versions.Get "postgres")}}

PostgreSQL

Журнал сделок m2m-core и принимающая БД. Сейчас: {{if .Settings.Postgres.DSN}}подключено{{else}}in-memory (данные не сохраняются){{end}}.

@@ -70,6 +84,7 @@ {{/* ============ ОБМЕН FANCY (MSSQL) — перспективный модуль ============ */}}

Обмен Fancy (MSSQL / PostgreSQL)

+ {{template "verline" (.Versions.Get "depo")}}

Подключение к серверу БД клиента

Сервер учётной базы Fancy DEPO: картотека лиц, счета и договоры, остатки, опции (признак квалифицированного инвестора), трансферные таблицы обмена. Поддерживаются Microsoft SQL Server и PostgreSQL. База указывается ниже, в карточке «Трансферные таблицы DEPO».

@@ -177,6 +192,7 @@ {{/* ============ QUIK МНП ============ */}}

QUIK — Модуль неторговых поручений

+ {{template "verline" (.Versions.Get "quik-mnp")}}

Подключение к БД МНП QUIK (MSSQL)

При выводе бумаг клиентом bj блокирует их количество в QUIK через API Модуля неторговых поручений (хранимые процедуры). БД боевая — тест только на чтение (версия + наличие M2M-объектов), без записи. Подключение под логином с ролью InstrEu.
@@ -368,6 +384,7 @@ {{/* ============ КРИПТОГРАФИЯ ============ */}}

Криптография

+ {{template "verline" (.Versions.Get "crypto-service.jar")}}{{template "verline" (.Versions.Get "validata")}}{{template "verline" (.Versions.Get "java")}}

Установка ключа на флешку

@@ -451,6 +468,7 @@ {{/* ============ НРД ============ */}}

НРД

+ {{template "verline" (.Versions.Get "ish")}}

Интеграционный шлюз (ИШ)

{{if not .Settings.NSD.IGWBaseURL}}mock-режим — без реального ИШ.{{else}}Профиль {{.Settings.NSD.Profile}}, ИШ {{.Settings.NSD.IGWBaseURL}}.{{end}} Стенды и установка — справка.

@@ -508,6 +526,7 @@

Личный кабинет · ЕСИА Финанс

Интеграция с платформой ЛК ЕСИА Финанс: адрес для клиента, приём заявок M2M, возврат статуса и подключение к API (тест/бой). НРД настраивается отдельно — в разделе «НРД».

+ {{template "verline" (.Versions.Get "bj-lk")}}

Личный кабинет — интеграция

@@ -792,6 +811,7 @@ {{/* ============ ОБНОВЛЕНИЯ ============ */}}

Обновления

+ {{template "verline" (.Versions.Get "bj-server")}}

Компоненты

@@ -841,6 +861,7 @@

Модуль «НПР / УДС» (риск-монитор по данным QUIK)

Показывает НПР1 / НПР2 / УДС клиентов маржинальной торговли по базе экспорта QUIK (схема npr). Отдельно лицензируемый модуль (признак npr); при включении в меню появляется пункт «НПР / УДС». Подключение — логин только для чтения.

+ {{template "verline" (.Versions.Get "bj-npr")}}
@@ -991,3 +1012,4 @@ })(); {{end}} +{{define "verline"}}{{if .Title}}

{{.Title}}: {{if .Version}}{{.Version}}{{else}}не определена{{end}}{{with .Detail}} · {{.}}{{end}}{{with .Err}} · {{.}}{{end}}

{{end}}{{end}} diff --git a/modules/quikexport/exporter_version.go b/modules/quikexport/exporter_version.go new file mode 100644 index 0000000..b521a48 --- /dev/null +++ b/modules/quikexport/exporter_version.go @@ -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 +} diff --git a/modules/quikexport/exporter_version_test.go b/modules/quikexport/exporter_version_test.go new file mode 100644 index 0000000..52a26ac --- /dev/null +++ b/modules/quikexport/exporter_version_test.go @@ -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) + } +} diff --git a/modules/quikexport/quikexport.go b/modules/quikexport/quikexport.go index ef3186a..f5b892f 100644 --- a/modules/quikexport/quikexport.go +++ b/modules/quikexport/quikexport.go @@ -856,6 +856,9 @@ type QuikExportView struct { ShowRO bool ServerIP string Warnings []string + + ModuleVersion string // bj-quikexport + ExporterVersion string // ARQA quikexport (\"\" — не определена) } var ( diff --git a/modules/quikexport/server.go b/modules/quikexport/server.go index b89097e..d4fcf72 100644 --- a/modules/quikexport/server.go +++ b/modules/quikexport/server.go @@ -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) diff --git a/modules/quikexport/web/section.html b/modules/quikexport/web/section.html index d5c8774..e02e30f 100644 --- a/modules/quikexport/web/section.html +++ b/modules/quikexport/web/section.html @@ -2,6 +2,7 @@ {{$X := .}}{{$S := $X.S}}

QUIK — Экспорт биржевой информации

Модуль ARQA «Экспорт биржевой информации» выгружает данные сервера QUIK (портфели клиентов, позиции, сделки, справочники) в базу PostgreSQL. bj управляет им отсюда: ключи и логин QUIK, где держим базу, какой объём данных выгружаем, состояние службы и место на диске. На этих данных работают модуль «НПР / УДС» и дополнительные проверки M2M. Лицензионный признак quik-export.

+

Экспортёр QUIK (ARQA quikexport): {{if $X.ExporterVersion}}{{$X.ExporterVersion}}{{else}}не определена{{end}} · модуль bj-quikexport{{with $X.ModuleVersion}} {{.}}{{end}}

{{range $X.Warnings}}
{{.}}
{{end}}
diff --git a/scripts/build-release.sh b/scripts/build-release.sh index a48a18d..0a0cb17 100755 --- a/scripts/build-release.sh +++ b/scripts/build-release.sh @@ -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