Files
Bridge-and-Join-s/internal/lkgateway/units.go
T
zuevav 2edc83e614 Модуль, снятый с лицензии, отключается один раз: служба остановлена (bj-keymedia unit-disable), одно оповещение, в списках не показывается; вернули в лицензию — служба включается
Также: версия файла, совпадающего с выпуском, записывается (нет ложного «новее»); признак неудачной проверки отдельно от результатов установки; окно обновления само перезагружает страницу.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
2026-09-24 22:43:34 +03:00

131 lines
4.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package lkgateway
// units.go — состояние служб bj и её модулей для страницы настроек.
//
// Модули вынесены в отдельные службы (bj-npr, bj-quikexport, bj-lk), и когда одна
// из них лежит, раздел модуля в bj отвечает ошибкой связи. Раньше администратору
// оставалось идти в консоль за systemctl status; здесь он видит состояние всех
// служб и может перезапустить нужную. Читаем состояние обычным systemctl show
// (прав не требует), перезапуск делает root-демон bj-keymedia по закрытому списку.
import (
"context"
"net/http"
"os/exec"
"strings"
"time"
"git.zetit.ru/zuevav/Bridge-and-Join-s/internal/release"
)
// UnitView — служба для карточки настроек.
type UnitView struct {
Unit string // bj-npr
Title string // Риск-монитор НПР / УДС
Core bool // ядро (перезапуск рвёт текущую страницу)
State string // active | inactive | failed | unknown
Sub string // running, dead, …
Since string // с какого времени в этом состоянии
Result string // причина отказа (systemd Result)
Present bool // компонент установлен на этом сервере
}
// OK — служба работает.
func (u UnitView) OK() bool { return u.State == "active" && u.Sub == "running" }
// unitState — состояние одной службы (без прав root).
func unitState(unit string) UnitView {
v := UnitView{Unit: unit, State: "unknown"}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
out, err := exec.CommandContext(ctx, "systemctl", "show", unit,
"--property=ActiveState,SubState,ActiveEnterTimestamp,Result").Output()
if err != nil {
return v
}
for _, line := range strings.Split(string(out), "\n") {
k, val, ok := strings.Cut(strings.TrimSpace(line), "=")
if !ok || val == "" {
continue
}
switch k {
case "ActiveState":
v.State = val
case "SubState":
v.Sub = val
case "Result":
if val != "success" {
v.Result = val
}
case "ActiveEnterTimestamp":
// «Wed 2026-09-16 01:13:56 MSK» — оставляем дату и время
if f := strings.Fields(val); len(f) >= 3 {
v.Since = f[1] + " " + f[2]
}
}
}
return v
}
// unitViews — службы bj на этом сервере: ядро и установленные модули.
func unitViews(rc *RuntimeConfig) []UnitView {
installed := release.LoadInstalled(release.InstalledFile)
var out []UnitView
for _, c := range release.Components {
if c.Unit == "" {
continue
}
_, present := installed[c.Name]
if !present && !c.Core {
continue // модуля на сервере нет — и службы быть не должно
}
if moduleOff(rc, c.Name) {
continue // снят с лицензии: служба отключена намеренно
}
v := unitState(c.Unit)
v.Title, v.Core, v.Present = c.Title, c.Core, true
out = append(out, v)
}
return out
}
// restartUnit — перезапуск службы через root-демон (см. bj-keymedia-worker).
func restartUnit(ctx context.Context, unit string) (string, error) {
cctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()
res, err := runKeymediaOp(cctx, "unit-restart", map[string]string{"extra": unit})
if err != nil {
return "", err
}
if s, ok := res["output"].(string); ok && s != "" {
return s, nil
}
return "перезапущено: " + unit, nil
}
// unitRestart — POST /admin/setup/units/restart: перезапуск службы из настроек.
func (h *setupHandlers) unitRestart(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method", http.StatusMethodNotAllowed)
return
}
unit := strings.TrimSpace(r.FormValue("unit"))
known := false
for _, c := range release.Components {
// пустой Unit у bj-component-install — не служба; пустая строка не должна проходить
if c.Unit != "" && c.Unit == unit {
known = true
}
}
if !known {
setupFlash(w, r, "Неизвестная служба.")
return
}
msg, err := restartUnit(r.Context(), unit)
if err != nil {
setupFlash(w, r, "Перезапуск "+unit+": "+err.Error())
return
}
setupFlash(w, r, msg)
}