Экспорт QUIK — отдельная служба bj-quikexport; модули, выделенные из ядра, ставятся с обновлением

bj-quikexport (modules/quikexport + cmd/bj-quikexport, 127.0.0.1:8084): управление ARQA
quikexport перенесено из bj-server. Настройки у службы в /var/lib/bj/.bj/quik-export/settings.json
(при первом запуске переносятся из setup.json), раздел отдаётся фрагментом с индикатором меню,
действия отвечают X-BJ-Flash. bj-server встраивает раздел в «Настройку», проксирует формы
/admin/setup/quikexport/… и передаёт лицензию (X-BJ-Licensed). Общая проверка служебного
ключа — modules/modkit (им пользуется и bj-npr), общее для прокси — lkgateway/modules.go.

Выделенные из ядра модули (bj-npr, bj-quikexport: Component.FromCore) при обновлении ставятся и
там, где их ещё нет, если входят в лицензию: release.MarkSplitModules, request.json "new",
root-установщик ставит и включает unit-файл из комплекта. Если в обновлении меняется сам
установщик, модули ставятся после перезапуска bj-server (ensureSplitModules).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
zuevav
2026-09-16 01:15:27 +03:00
parent 5527a464ca
commit 97e99b72c7
26 changed files with 1095 additions and 303 deletions
+45 -2
View File
@@ -11,6 +11,7 @@ import (
"errors"
"fmt"
"io"
"io/fs"
"os"
"os/exec"
"path/filepath"
@@ -29,6 +30,7 @@ type InstallOptions struct {
PubKey ed25519.PublicKey // открытый ключ подписи релизов
Restart bool // перезапустить службы установленных компонентов (ядро последним)
AllowNew bool // ставить модули, которых ещё нет на сервере (первичная установка)
Units fs.FS // unit-файлы служб (systemd/<служба>.service) для впервые устанавливаемых модулей
}
// InstallResult — итог установки (JSON для bj-server).
@@ -68,6 +70,7 @@ func InstallStaged(staging string, opt InstallOptions) (res InstallResult) {
res.Release = m.Version
var req struct {
Names []string `json:"names"`
New []string `json:"new"` // выделенные из ядра модули, которые ставятся впервые
}
if err := readStagedJSON(staging, "request.json", &req); err != nil {
return fail("запрос: %v", err)
@@ -105,7 +108,7 @@ func InstallStaged(staging string, opt InstallOptions) (res InstallResult) {
if art.Version == "" {
art.Version = m.Version
}
if _, err := os.Stat(c.PathAt(opt.Root)); err != nil && !c.Core && !opt.AllowNew {
if _, err := os.Stat(c.PathAt(opt.Root)); err != nil && !c.Core && !opt.AllowNew && !(c.FromCore && contains(req.New, name)) {
return fail("%s не установлен на этом сервере: модуль ставится установщиком, а не обновлением", name)
}
if prev, ok := installed[name]; ok && prev.Version != "" && IsNewer(prev.Version, art.Version) {
@@ -124,7 +127,7 @@ func InstallStaged(staging string, opt InstallOptions) (res InstallResult) {
sort.SliceStable(jobs, func(i, j int) bool { return componentOrder(jobs[i].c.Name) < componentOrder(jobs[j].c.Name) })
ts := time.Now().Format("20060102-150405")
var units []string
var units, newUnits []string
for _, j := range jobs {
if err := installFile(j.file, j.c.PathAt(opt.Root), j.c.Exec, ts); err != nil {
return fail("установка %s: %v", j.c.Name, err)
@@ -133,6 +136,21 @@ func InstallStaged(staging string, opt InstallOptions) (res InstallResult) {
res.Installed = append(res.Installed, map[string]string{"name": j.c.Name, "version": j.a.Version})
if j.c.Unit != "" {
units = append(units, j.c.Unit)
added, err := ensureUnit(opt.Root, opt.Units, j.c.Unit)
if err != nil {
return fail("служба %s: %v", j.c.Unit, err)
}
if added {
newUnits = append(newUnits, j.c.Unit)
}
}
}
if len(newUnits) > 0 && opt.Root == "" {
if out, err := exec.Command("systemctl", "daemon-reload").CombinedOutput(); err != nil {
return fail("systemctl daemon-reload: %v: %s", err, strings.TrimSpace(string(out)))
}
if out, err := exec.Command("systemctl", append([]string{"enable"}, newUnits...)...).CombinedOutput(); err != nil {
return fail("включение служб %s: %v: %s", strings.Join(newUnits, ", "), err, strings.TrimSpace(string(out)))
}
}
if err := SaveInstalled(installedPath, installed); err != nil {
@@ -153,6 +171,31 @@ func InstallStaged(staging string, opt InstallOptions) (res InstallResult) {
return res
}
// ensureUnit — поставить unit-файл службы из комплекта, если его ещё нет (true — поставлен).
func ensureUnit(root string, units fs.FS, unit string) (bool, error) {
dst := filepath.Join(root, "/etc/systemd/system", unit+".service")
if _, err := os.Stat(dst); err == nil || units == nil {
return false, nil
}
b, err := fs.ReadFile(units, "systemd/"+unit+".service")
if err != nil {
return false, nil // в комплекте нет — служба ставится установщиком
}
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
return false, err
}
return true, os.WriteFile(dst, b, 0o644)
}
func contains(list []string, v string) bool {
for _, x := range list {
if x == v {
return true
}
}
return false
}
func componentOrder(name string) int {
for i, c := range Components {
if c.Name == name {