Обновление панели из интерфейса и режим обновления в установщике
Установщик стал идемпотентным: на уже настроенной машине он определяет установленную версию, останавливает службу, меняет бинарник и поднимает её обратно, не трогая конфиг, миры и учётные записи. Зависимости при обновлении не переустанавливаются — это лишние минуты apt. Кнопки «Обновить» и «Перезапустить панель» появились на вкладке «Настройки» вместе с текущей версией и описанием нового релиза. Панель работает от непривилегированного пользователя и не может ни заменить свой бинарник в /usr/local/bin, ни дёрнуть systemctl. Вместо того чтобы расширять её права, установщик кладёт root-скрипт pzmanager-admin и разрешает сервисному пользователю через sudoers ровно эту одну команду. Скрипт сверяет sha256 скачанного бинарника с checksums.txt релиза. Если правила sudo нет, кнопки не будет — панель покажет команду для терминала. Перед обновлением и перезапуском игровой сервер останавливается штатным quit: иначе его погасил бы systemd уже в момент рестарта, и ожидание растянулось бы на таймаут остановки. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/mrleo1nid/pzmanager/internal/pzserver"
|
||||
"github.com/mrleo1nid/pzmanager/internal/updater"
|
||||
)
|
||||
|
||||
// handleCheckUpdate спрашивает у репозитория последнюю версию.
|
||||
func (s *Server) handleCheckUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, s.updater.Check(r.Context()))
|
||||
}
|
||||
|
||||
// handleApplyUpdate качает новую версию и перезапускает панель. Ответ уходит
|
||||
// раньше, чем работа заканчивается: обновление убивает текущий процесс.
|
||||
func (s *Server) handleApplyUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
if r.ContentLength > 0 && !decodeJSON(w, r, &body) {
|
||||
return
|
||||
}
|
||||
|
||||
if !s.updater.CanApply(r.Context()) {
|
||||
writeError(w, http.StatusPreconditionFailed, updater.ErrNoAdmin.Error())
|
||||
return
|
||||
}
|
||||
|
||||
name := "Обновление панели"
|
||||
err := s.jobs.Start(name, func(ctx context.Context, log func(string)) error {
|
||||
// Игровой сервер останавливаем сами: иначе это сделает systemd уже в
|
||||
// момент перезапуска, и ожидание растянется на таймаут остановки.
|
||||
s.stopServerBefore(log, "обновлением")
|
||||
log("Запускаю обновление, панель перезапустится")
|
||||
return s.updater.Apply(ctx, log, body.Version)
|
||||
})
|
||||
if err != nil {
|
||||
writeOpError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusAccepted, map[string]any{"task": name})
|
||||
}
|
||||
|
||||
// handleRestartManager перезапускает саму панель.
|
||||
func (s *Server) handleRestartManager(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.updater.CanApply(r.Context()) {
|
||||
writeError(w, http.StatusPreconditionFailed, updater.ErrNoAdmin.Error())
|
||||
return
|
||||
}
|
||||
|
||||
name := "Перезапуск панели"
|
||||
err := s.jobs.Start(name, func(ctx context.Context, log func(string)) error {
|
||||
s.stopServerBefore(log, "перезапуском")
|
||||
log("Перезапускаю панель")
|
||||
return s.updater.Restart(ctx, log)
|
||||
})
|
||||
if err != nil {
|
||||
writeOpError(w, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusAccepted, map[string]any{"task": name})
|
||||
}
|
||||
|
||||
// stopServerBefore корректно гасит игровой сервер перед тем, как панель уйдёт
|
||||
// в перезапуск: мир сохраняется, а systemd не ждёт таймаут остановки.
|
||||
func (s *Server) stopServerBefore(log func(string), what string) {
|
||||
if s.sup.State() == pzserver.StateStopped {
|
||||
return
|
||||
}
|
||||
log("Останавливаю игровой сервер перед " + what)
|
||||
if err := s.sup.Stop(); err != nil && !errors.Is(err, pzserver.ErrNotRunning) {
|
||||
log("Остановить сервер не удалось: " + err.Error())
|
||||
return
|
||||
}
|
||||
s.sup.WaitStopped(30 * time.Second)
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/mrleo1nid/pzmanager/internal/pzconfig"
|
||||
"github.com/mrleo1nid/pzmanager/internal/pzserver"
|
||||
"github.com/mrleo1nid/pzmanager/internal/steamcmd"
|
||||
"github.com/mrleo1nid/pzmanager/internal/updater"
|
||||
)
|
||||
|
||||
// csrfHeader — заголовок, который обязан прислать клиент при изменяющих
|
||||
@@ -34,6 +35,7 @@ type Server struct {
|
||||
backups *backup.Manager
|
||||
steam *steamcmd.Client
|
||||
workshop *pzconfig.WorkshopClient
|
||||
updater *updater.Updater
|
||||
static fs.FS
|
||||
// secureCookies включает флаг Secure у cookie сессии (панель за HTTPS).
|
||||
secureCookies bool
|
||||
@@ -55,6 +57,8 @@ type Options struct {
|
||||
Static fs.FS
|
||||
SecureCookies bool
|
||||
SetupToken string
|
||||
// Repo — репозиторий, из которого панель берёт обновления.
|
||||
Repo string
|
||||
}
|
||||
|
||||
// New собирает HTTP-обработчик панели.
|
||||
@@ -68,6 +72,7 @@ func New(o Options) *Server {
|
||||
backups: o.Backups,
|
||||
steam: o.Steam,
|
||||
workshop: pzconfig.NewWorkshopClient(),
|
||||
updater: updater.New(o.Repo, o.Version),
|
||||
static: o.Static,
|
||||
secureCookies: o.SecureCookies,
|
||||
setupToken: o.SetupToken,
|
||||
@@ -114,6 +119,9 @@ func (s *Server) Handler() http.Handler {
|
||||
"GET /api/backups/{name}/download": s.handleDownloadBackup,
|
||||
"POST /api/backups/{name}/restore": s.handleRestoreBackup,
|
||||
"POST /api/install": s.handleInstall,
|
||||
"GET /api/update": s.handleCheckUpdate,
|
||||
"POST /api/update": s.handleApplyUpdate,
|
||||
"POST /api/manager/restart": s.handleRestartManager,
|
||||
"GET /api/task": s.handleTaskStatus,
|
||||
"POST /api/task/cancel": s.handleTaskCancel,
|
||||
"GET /api/users": s.handleListUsers,
|
||||
|
||||
Reference in New Issue
Block a user