2edc83e614
Также: версия файла, совпадающего с выпуском, записывается (нет ложного «новее»); признак неудачной проверки отдельно от результатов установки; окно обновления само перезагружает страницу. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014FsEKyJrYF69Cj9ZXzFLRi
131 lines
4.6 KiB
Go
131 lines
4.6 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 // модуля на сервере нет — и службы быть не должно
|
||
}
|
||
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)
|
||
}
|