Files
Bridge-and-Join-s/internal/lkgateway/units.go
T
zuevav 414227796b Настройка: состояние служб ядра и модулей с перезапуском из админки
Модули работают отдельными службами (bj-npr, bj-quikexport, bj-lk), и когда
служба лежит, раздел модуля отвечает ошибкой связи, а администратору
предлагалось идти в консоль за systemctl status. Теперь в разделе «Обновления»
карточка «Службы»: состояние каждой службы, с какого времени, причина отказа
и кнопка «Перезапустить».

Состояние читаем обычным systemctl show — прав не требует. Перезапуск делает
root-демон bj-keymedia (тот же канал, что у установки обновлений) по закрытому
списку служб: чужую службу и сам демон отсюда не тронуть.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4YVo4UMopqT5DzicqpPjL
2026-09-18 19:28:58 +03:00

127 lines
4.3 KiB
Go

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 // модуля на сервере нет — и службы быть не должно
}
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 {
if 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)
}