Модуль, снятый с лицензии, отключается один раз: служба остановлена (bj-keymedia unit-disable), одно оповещение, в списках не показывается; вернули в лицензию — служба включается

Также: версия файла, совпадающего с выпуском, записывается (нет ложного «новее»); признак неудачной проверки отдельно от результатов установки; окно обновления само перезагружает страницу.

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-09-24 22:43:34 +03:00
parent e450887688
commit 2edc83e614
7 changed files with 192 additions and 12 deletions
+63 -3
View File
@@ -10,9 +10,12 @@ package lkgateway
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"os"
"path/filepath"
@@ -95,6 +98,7 @@ func (u *Updater) CheckForUpdate(ctx context.Context) (UpdateStatus, error) {
return st, err
}
st.Available, st.Notes = m.Version, m.Notes
syncSameFileVersions(m)
st.Components = release.Plan("", m, release.LoadInstalled(release.InstalledFile), BuildVersion)
release.MarkSplitModules(st.Components, u.allowed)
off := markUnlicensed(st.Components, u.allowed)
@@ -121,9 +125,59 @@ func (u *Updater) CheckForUpdate(ctx context.Context) (UpdateStatus, error) {
return st, nil
}
// syncSameFileVersions — если файл компонента на сервере побайтно совпадает с файлом выпуска, а
// записана другая версия (компонент не менялся, а номер выпуска вырос, или файл выложен вручную),
// записываем версию выпуска: иначе список предлагает «обновить» то, что установщик отклонит как
// «обновление не требуется».
func syncSameFileVersions(m *release.Manifest) {
installed := release.LoadInstalled(release.InstalledFile)
changed := false
for _, a := range m.Artifacts {
c, ok := release.ComponentByName(a.Name)
if !ok || a.SHA256 == "" {
continue
}
v := a.Version
if v == "" {
v = m.Version
}
if cur, ok := installed[a.Name]; ok && cur.Version == v {
continue
}
sum, err := fileSHA256(c.Path)
if err != nil || !strings.EqualFold(sum, a.SHA256) {
continue
}
installed[a.Name] = release.Installed{Version: v, SHA256: sum, InstalledAt: time.Now().UTC()}
changed = true
}
if changed {
if err := release.SaveInstalled(release.InstalledFile, installed); err != nil {
log.Printf("lk-gateway: запись версий совпадающих компонентов: %v", err)
}
}
}
func fileSHA256(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return "", err
}
return hex.EncodeToString(h.Sum(nil)), nil
}
// markUnlicensed — модули, установленные на сервере, но снятые с лицензии: не обновляются (сервер
// обновлений их файлы не отдаёт), в списке — с пояснением. Возвращает названия таких модулей.
func markUnlicensed(states []release.ComponentState, allowed func(string) bool) []string {
var disabled map[string]string
if globalRC != nil {
disabled = globalRC.Snapshot().Update.DisabledModules
}
var off []string
for i := range states {
c := &states[i]
@@ -131,7 +185,7 @@ func markUnlicensed(states []release.ComponentState, allowed func(string) bool)
continue
}
c.Update, c.New = false, false
if c.Present {
if _, already := disabled[c.Name]; c.Present && !already { // об отключённом уже сказано один раз
c.Blocked = "не входит в лицензию — модуль отключён и не обновляется; чтобы подключить, свяжитесь с ZETIT"
off = append(off, "«"+c.Title+"»")
}
@@ -163,6 +217,7 @@ func (u *Updater) saveCheckResult(st UpdateStatus, avail map[string]string) {
cfg := u.rc.Snapshot().Update
cfg.LastCheck = time.Now().UTC()
cfg.LastResult = st.Message
cfg.CheckFailed = avail == nil // nil — манифест получить не удалось
cfg.Available = st.Available
cfg.Notes = st.Notes
if avail != nil {
@@ -202,6 +257,7 @@ func (u *Updater) applyUpdate(ctx context.Context, names []string) (string, erro
if err != nil {
return "", fmt.Errorf("манифест: %w", err)
}
syncSameFileVersions(m)
plan := release.Plan("", m, release.LoadInstalled(release.InstalledFile), BuildVersion)
release.MarkSplitModules(plan, u.allowed)
off := markUnlicensed(plan, u.allowed)
@@ -414,7 +470,10 @@ func updateComponentRows(rc *RuntimeConfig) ([]release.ComponentState, bool) {
r.Update = r.Available != "" && (r.Present || r.Core) && (r.Installed == "" || release.IsNewer(r.Available, r.Installed))
// Модуль, которого нет на сервере и нет в лицензии, не показываем: поставить его нельзя,
// а в списке он выглядит так, будто входит в поставку.
if r.Present || r.Core || (r.Available != "" && moduleAllowed(rc, r.Feature)) {
// Модуль вне лицензии не показываем вовсе (и установленный: его служба отключена), пока
// лицензия действует; без действующей лицензии список как есть.
unlicensed := !r.Core && r.Feature != "" && r.Feature != "updates" && licenseStatus(rc).Valid && !moduleAllowed(rc, r.Feature)
if !unlicensed && (r.Present || r.Core || (r.Available != "" && moduleAllowed(rc, r.Feature))) {
out = append(out, r)
}
}
@@ -422,7 +481,7 @@ func updateComponentRows(rc *RuntimeConfig) ([]release.ComponentState, bool) {
markUnlicensed(out, func(f string) bool { return moduleAllowed(rc, f) })
// Последняя проверка не удалась — предлагать установку по старым данным нельзя:
// кнопка ушла бы в тот же отказ. Показываем причину и просим проверить заново.
if updateResultFailed(set.LastResult) {
if set.CheckFailed {
for i := range out {
if out[i].Update && out[i].Available != "" {
out[i].Blocked = "проверка обновлений не удалась — нажмите «Проверить обновления»"
@@ -476,6 +535,7 @@ func (u *Updater) updateLoop(ctx context.Context) {
return
case <-time.After(time.Minute):
cleanupStaging(time.Hour)
enforceModuleLicense(ctx, u.rc)
u.ensureSplitModules(ctx)
check()
}