Add graceful SCUM stop, restart, and version update flow

SCUM 停止/重启/更新以前只有“结束进程”这一条路,插件没有声明任何优雅关闭方式,
平台也没有把停止后重新启动串起来。现在插件声明自己的关闭脚本,run 先执行它,
平台在停止或更新成功后再自动拉起服务。

run:
- lifecycle stop 支持插件声明的 gracefulStop(可执行文件、参数、环境、超时、
  fallback=report|terminate);关闭命令超时且声明 report 时任务失败,不再默默杀进程。
- 新增 steam.update 依赖探针:调用 steamcmd +app_info_print 获取公开分支 buildid,
  与本地 steamapps/appmanifest_<appid>.acf 的 buildid 比较,输出
  installed/latest/update=yes|no|unknown。

platform:
- 新增 POST /api/v1/server-instances/{id}/restart 与 /update。
- restart 派发插件 stop 动作(走优雅关闭),终态成功后入队 start 作业。
- update 派发插件 install 动作;插件在更新前必须先优雅关闭 SCUM,关闭失败直接拒绝
  SteamCMD 更新,成功后平台再拉起服务。
- 依赖检查输入带上插件声明的服务器安装根目录,供 steam.update 读取 appmanifest。

plugin (SCUM server plugin 0.1.16):
- bin/scum-stop.cmd:解析已声明的可执行文件路径,定位同路径正在运行的 SCUMServer.exe,
  通过本地 RCON 公告并发送关闭命令,等待进程自行退出;不再使用 taskkill。
- bin/scum-rcon.ps1:插件自有的 Source RCON 客户端,从 UE4SS mod config.ini 读取
  密码/端口,密钥不离开本机。
- actions/stop.json 声明 gracefulStop;actions/install.json 更新前先执行同一关闭脚本。

platform_web:
- 服务器详情新增“重启”按钮和“SCUM 版本更新”面板;点“检查更新”查询公开分支版本,
  只有检测到更新时“更新版本”按钮才会置为可用并高亮,点击后先确认再派发更新任务。
This commit is contained in:
npc0-hue
2026-09-15 13:30:15 +08:00
parent 4ea27bda6a
commit 05f5a97ba9
39 changed files with 1197 additions and 56 deletions
+2
View File
@@ -63,6 +63,8 @@ func (h *coreHandlers) register(mux *http.ServeMux) {
mux.HandleFunc("/api/v1/server-instances/workflows/create", h.serverInstanceCreateWorkflow)
mux.HandleFunc("/api/v1/server-instances/{id}/start", h.serverInstanceStart)
mux.HandleFunc("/api/v1/server-instances/{id}/stop", h.serverInstanceStop)
mux.HandleFunc("/api/v1/server-instances/{id}/restart", h.serverInstanceRestart)
mux.HandleFunc("/api/v1/server-instances/{id}/update", h.serverInstanceUpdate)
mux.HandleFunc("/api/v1/server-instances/{id}/process/status", h.serverInstanceProcessStatus)
mux.HandleFunc("/api/v1/server-instances/{id}/runtime/actions", h.serverRuntimeActions)
mux.HandleFunc("/api/v1/server-instances/{id}/runtime-binding", h.serverRuntimeBinding)
+3 -1
View File
@@ -134,6 +134,8 @@ Artifact bridge execution returns safe metadata and platform content routes only
- `POST /api/v1/server-instances/workflows/create`: accepts `ServerLifecycleCreateRequest`. Creation starts from `pluginId` and `name`, and may include the create-wizard deployment definition such as deployment mode, plugin create inputs, server root, or custom start command. The browser does not submit a deployment target, Run endpoint, lifecycle profile, or Run identity binding. Platform applies plugin defaults, creates the definition without waiting for a Run, and attaches the active Run when its authenticated heartbeat arrives. Unknown legacy binding fields are rejected by the strict JSON decoder.
- `POST /api/v1/server-instances/{id}/start`: accept `ServerLifecycleCommandRequest`, validate state/config version/run capability, and queue a `process.start` job using `ServerLifecycleResponse`.
- `POST /api/v1/server-instances/{id}/stop`: accept `ServerLifecycleCommandRequest`, validate state/config version/run capability, and queue a `process.stop` job using `ServerLifecycleResponse`.
- `POST /api/v1/server-instances/{id}/restart`: accept `ServerLifecycleCommandRequest`, validate the instance is `running` or `stopped` with a matching config version and run capability, and queue the plugin-declared stop action using `ServerLifecycleResponse`. When that stop job succeeds, Platform queues the plugin-declared start action for the same instance.
- `POST /api/v1/server-instances/{id}/update`: accept `ServerLifecycleCommandRequest`, validate the instance is `running`, `stopped`, `ready`, or `failed` with a matching config version and `process.install` run capability, and queue the plugin-declared install/update action using `ServerLifecycleResponse`. The plugin install action owns the graceful close of a running server; Platform queues the plugin-declared start action after the update succeeds.
Lifecycle workflow responses include accepted status, action, bounded server instance metadata, and bounded job metadata. They do not expose run credentials, host paths, raw credentials, AI provider keys, direct sockets, plugin action file contents, or large result bodies.
@@ -251,7 +253,7 @@ These route groups remain documented future work beyond the currently implemente
- Plugin page iframe packaging and remote hosting policies beyond SDK-mediated bridge contracts.
- Live AI provider connectivity tests and remote model discovery.
- Production Run distribution signing/KMS, fleet rollout rings, and real AI-provider integration.
- Server restart/delete routes beyond the currently implemented lifecycle, metadata update, and archive actions.
- Server delete/archive routes beyond the currently implemented lifecycle, metadata update, and archive actions.
## Core Service Boundary
+62
View File
@@ -160,6 +160,68 @@ func (h *coreHandlers) serverInstanceStop(w http.ResponseWriter, r *http.Request
writeJSON(w, http.StatusOK, dto.ServerLifecycleFromDomain(result))
}
// serverInstanceRestart godoc
// @Summary Restart server instance
// @Description Validates lifecycle state and config version, queues the plugin-declared stop action, and queues the plugin-declared start action after the graceful stop result lands.
// @Tags server-instances
// @Accept json
// @Produce json
// @Param id path string true "Server instance ID"
// @Param body body dto.ServerLifecycleCommandRequest true "Server lifecycle command request"
// @Success 202 {object} dto.ServerLifecycleResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/server-instances/{id}/restart [post]
func (h *coreHandlers) serverInstanceRestart(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.ServerLifecycleCommandRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
result, err := h.core.RestartServerInstanceForSession(bearerToken(r), request.ToDomain(r.PathValue("id")))
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusAccepted, dto.ServerLifecycleFromDomain(result))
}
// serverInstanceUpdate godoc
// @Summary Update server game files
// @Description Validates lifecycle state and config version, queues the plugin-declared install/update action, and queues the plugin-declared start action after the update result lands.
// @Tags server-instances
// @Accept json
// @Produce json
// @Param id path string true "Server instance ID"
// @Param body body dto.ServerLifecycleCommandRequest true "Server lifecycle command request"
// @Success 202 {object} dto.ServerLifecycleResponse
// @Failure 400 {object} dto.ErrorResponse
// @Failure 404 {object} dto.ErrorResponse
// @Failure 405 {object} dto.ErrorResponse
// @Router /api/v1/server-instances/{id}/update [post]
func (h *coreHandlers) serverInstanceUpdate(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeMethodNotAllowed(w, http.MethodPost)
return
}
request, err := decodeJSON[dto.ServerLifecycleCommandRequest](r)
if err != nil {
writeDecodeError(w, err)
return
}
result, err := h.core.UpdateServerGameForSession(bearerToken(r), request.ToDomain(r.PathValue("id")))
if err != nil {
writeServiceError(w, err)
return
}
writeJSON(w, http.StatusAccepted, dto.ServerLifecycleFromDomain(result))
}
// serverInstanceProcessStatus godoc
// @Summary Query supervised server process status
// @Description Queues a typed process.status job for the selected server/profile.