Files
browser/platform/dto/runtime_profiles.go
T
npc0-hue 05f5a97ba9 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 版本更新”面板;点“检查更新”查询公开分支版本,
  只有检测到更新时“更新版本”按钮才会置为可用并高亮,点击后先确认再派发更新任务。
2026-09-15 13:30:15 +08:00

336 lines
18 KiB
Go

package dto
import (
"encoding/json"
"fmt"
"net/url"
"path"
"browser.local/platform/domain"
)
type RuntimeTargetBody struct {
OS string `json:"os"`
Arch string `json:"arch"`
}
type RuntimeDiscoveryProbeBody struct {
Key string `json:"key"`
Kind string `json:"kind"`
TargetKey string `json:"targetKey"`
Required bool `json:"required,omitempty"`
Expected string `json:"expected,omitempty"`
Platforms []string `json:"platforms,omitempty"`
}
type RuntimeLifecycleProfileBody struct {
Key string `json:"key"`
Mode string `json:"mode"`
Capabilities []string `json:"capabilities"`
ActionRefs PluginLifecycleActionsBody `json:"actionRefs,omitempty"`
TransportKeys []string `json:"transportKeys,omitempty"`
ClientManagerRef json.RawMessage `json:"clientManagerRef,omitempty"`
DLLExtensionRefs []string `json:"dllExtensionRefs,omitempty"`
Platforms []string `json:"platforms,omitempty"`
}
type RuntimeDependencyProbeBody struct {
Key string `json:"key"`
Kind string `json:"kind"`
TargetKey string `json:"targetKey"`
Required bool `json:"required,omitempty"`
MinimumVersion string `json:"minimumVersion,omitempty"`
SteamAppID string `json:"steamAppId,omitempty"`
Platforms []string `json:"platforms,omitempty"`
}
type RuntimeInstallStepBody struct {
Type string `json:"type"`
TargetKey string `json:"targetKey"`
PackageManager string `json:"packageManager,omitempty"`
PackageName string `json:"packageName,omitempty"`
Version string `json:"version,omitempty"`
DownloadRef string `json:"downloadRef,omitempty"`
Checksum string `json:"checksum,omitempty"`
}
type RuntimeInstallPlanBody struct {
Key string `json:"key"`
Title string `json:"title"`
Platforms []string `json:"platforms,omitempty"`
Steps []RuntimeInstallStepBody `json:"steps"`
}
type RuntimeServerConfigMappingBody struct {
FieldKey string `json:"fieldKey"`
ConfigKey string `json:"configKey"`
ValueType string `json:"valueType"`
Required bool `json:"required,omitempty"`
}
type RuntimeServerDiscoveryMarkerBody struct {
Key string `json:"key"`
Kind string `json:"kind"`
TargetKey string `json:"targetKey"`
Expected string `json:"expected,omitempty"`
Required bool `json:"required,omitempty"`
}
type RuntimeServerVerificationCheckBody struct {
Key string `json:"key"`
Kind string `json:"kind"`
TargetKey string `json:"targetKey"`
Required bool `json:"required,omitempty"`
}
type RuntimeServerPrerequisiteBody struct {
Key string `json:"key"`
Kind string `json:"kind"`
}
type RuntimeLogSourceBody struct {
Key string `json:"key"`
Kind string `json:"kind"`
TargetKey string `json:"targetKey,omitempty"`
StreamKey string `json:"streamKey"`
CursorKind string `json:"cursorKind,omitempty"`
RetentionDays int `json:"retentionDays,omitempty"`
}
type RuntimeTransportProfileBody struct {
Key string `json:"key"`
Kind string `json:"kind"`
TargetKey string `json:"targetKey,omitempty"`
Capabilities []string `json:"capabilities"`
}
type RuntimeDataTargetBody struct {
Key string `json:"key"`
Kind string `json:"kind"`
TransportKey string `json:"transportKey"`
SourceRootKey string `json:"sourceRootKey"`
SourcePath string `json:"sourcePath"`
WorkspaceKey string `json:"workspaceKey"`
RefreshPolicy string `json:"refreshPolicy"`
MaxBytes int64 `json:"maxBytes"`
Platforms []string `json:"platforms,omitempty"`
}
type RuntimeRepositoryBody struct {
URL string `json:"url"`
RevisionPolicy string `json:"revisionPolicy"`
Branch string `json:"branch,omitempty"`
Tag string `json:"tag,omitempty"`
Revision string `json:"revision,omitempty"`
}
type RuntimeBuildBody struct {
System string `json:"system"`
WorkspaceRef string `json:"workspaceRef,omitempty"`
EntryRef string `json:"entryRef,omitempty"`
}
type RuntimeConfigTemplateBody struct {
Key string `json:"key"`
TemplateRef string `json:"templateRef"`
OutputRef string `json:"outputRef"`
}
type RuntimeDLLExtensionProfileBody struct {
Key string `json:"key"`
DisplayName string `json:"displayName"`
Kind string `json:"kind"`
Activation string `json:"activation"`
Version string `json:"version"`
ReleaseState string `json:"releaseState"`
ReleaseURL string `json:"releaseUrl,omitempty"`
ReleaseHost string `json:"releaseHost,omitempty"`
ReleaseFilename string `json:"releaseFilename,omitempty"`
Checksum string `json:"checksum,omitempty"`
SizeBytes int64 `json:"sizeBytes,omitempty"`
TargetKey string `json:"targetKey"`
ModKey string `json:"modKey"`
DLLRef string `json:"dllRef"`
TargetExecutableChecksum string `json:"targetExecutableChecksum,omitempty"`
UE4SSABI string `json:"ue4ssAbi,omitempty"`
SupportedTargets []RuntimeTargetBody `json:"supportedTargets"`
UpdateOnStart bool `json:"updateOnStart"`
RCONPort int `json:"rconPort"`
}
// RuntimeDLLExtensionProfileResponseBody is the browser-safe projection of a
// declared DLL extension. The immutable deployment path, RCON port, and full
// release URL remain internal to the manifest/start-job contracts.
type RuntimeDLLExtensionProfileResponseBody struct {
Key string `json:"key"`
DisplayName string `json:"displayName"`
Kind string `json:"kind"`
Activation string `json:"activation"`
Version string `json:"version"`
ReleaseState string `json:"releaseState"`
ReleaseHost string `json:"releaseHost,omitempty"`
ReleaseFilename string `json:"releaseFilename,omitempty"`
Checksum string `json:"checksum,omitempty"`
SizeBytes int64 `json:"sizeBytes,omitempty"`
TargetExecutableChecksum string `json:"targetExecutableChecksum,omitempty"`
UE4SSABI string `json:"ue4ssAbi,omitempty"`
SupportedTargets []RuntimeTargetBody `json:"supportedTargets"`
UpdateOnStart bool `json:"updateOnStart"`
}
type RuntimeDLLExtensionPlanBody struct {
Key string `json:"key"`
Version string `json:"version"`
ReleaseURL string `json:"releaseUrl"`
Checksum string `json:"checksum"`
SizeBytes int64 `json:"sizeBytes"`
TargetKey string `json:"targetKey"`
ModKey string `json:"modKey"`
DLLRef string `json:"dllRef"`
TargetExecutableChecksum string `json:"targetExecutableChecksum"`
UE4SSABI string `json:"ue4ssAbi"`
RCONPort int `json:"rconPort"`
}
type GamePluginRuntimeProfilesBody struct {
Discovery []RuntimeDiscoveryProbeBody `json:"discovery,omitempty"`
LifecycleProfiles []RuntimeLifecycleProfileBody `json:"lifecycleProfiles,omitempty"`
DependencyProbes []RuntimeDependencyProbeBody `json:"dependencyProbes,omitempty"`
InstallPlans []RuntimeInstallPlanBody `json:"installPlans,omitempty"`
ServerDeployments json.RawMessage `json:"serverDeployments,omitempty"`
LogSources []RuntimeLogSourceBody `json:"logSources,omitempty"`
TransportProfiles []RuntimeTransportProfileBody `json:"transportProfiles,omitempty"`
DataTargets []RuntimeDataTargetBody `json:"dataTargets,omitempty"`
ClientManagers json.RawMessage `json:"clientManagers,omitempty"`
DLLExtensions []RuntimeDLLExtensionProfileBody `json:"dllExtensions,omitempty"`
}
func (body GamePluginRuntimeProfilesBody) UnsupportedLegacyProfileViolations(prefix string) []string {
var violations []string
if len(body.ClientManagers) > 0 {
violations = append(violations, prefix+".clientManagers is no longer supported")
}
for i, profile := range body.LifecycleProfiles {
if len(profile.ClientManagerRef) > 0 {
violations = append(violations, fmt.Sprintf("%s.lifecycleProfiles[%d].clientManagerRef is no longer supported", prefix, i))
}
}
if len(body.ServerDeployments) > 0 {
violations = append(violations, prefix+".serverDeployments is no longer supported")
}
return violations
}
// GamePluginRuntimeProfilesResponseBody is intentionally distinct from the
// manifest input shape so browser responses cannot accidentally include
// machine-local DLL placement or RCON activation details.
type GamePluginRuntimeProfilesResponseBody struct {
Discovery []RuntimeDiscoveryProbeBody `json:"discovery,omitempty"`
LifecycleProfiles []RuntimeLifecycleProfileBody `json:"lifecycleProfiles,omitempty"`
DependencyProbes []RuntimeDependencyProbeBody `json:"dependencyProbes,omitempty"`
InstallPlans []RuntimeInstallPlanBody `json:"installPlans,omitempty"`
LogSources []RuntimeLogSourceBody `json:"logSources,omitempty"`
TransportProfiles []RuntimeTransportProfileBody `json:"transportProfiles,omitempty"`
DataTargets []RuntimeDataTargetBody `json:"dataTargets,omitempty"`
DLLExtensions []RuntimeDLLExtensionProfileResponseBody `json:"dllExtensions,omitempty"`
}
func (body GamePluginRuntimeProfilesBody) ToDomain() domain.GamePluginRuntimeProfiles {
profiles := domain.GamePluginRuntimeProfiles{}
for _, item := range body.Discovery {
profiles.Discovery = append(profiles.Discovery, domain.RuntimeDiscoveryProbe{Key: item.Key, Kind: item.Kind, TargetKey: item.TargetKey, Required: item.Required, Expected: item.Expected, Platforms: domain.CopyStringSlice(item.Platforms)})
}
for _, item := range body.LifecycleProfiles {
profiles.LifecycleProfiles = append(profiles.LifecycleProfiles, domain.RuntimeLifecycleProfile{Key: item.Key, Mode: item.Mode, Capabilities: domain.CopyStringSlice(item.Capabilities), ActionRefs: item.ActionRefs.ToDomain(), TransportKeys: domain.CopyStringSlice(item.TransportKeys), DLLExtensionRefs: domain.CopyStringSlice(item.DLLExtensionRefs), Platforms: domain.CopyStringSlice(item.Platforms)})
}
for _, item := range body.DependencyProbes {
profiles.DependencyProbes = append(profiles.DependencyProbes, domain.RuntimeDependencyProbe{Key: item.Key, Kind: item.Kind, TargetKey: item.TargetKey, Required: item.Required, MinimumVersion: item.MinimumVersion, SteamAppID: item.SteamAppID, Platforms: domain.CopyStringSlice(item.Platforms)})
}
for _, item := range body.InstallPlans {
plan := domain.RuntimeInstallPlan{Key: item.Key, Title: item.Title, Platforms: domain.CopyStringSlice(item.Platforms)}
for _, step := range item.Steps {
plan.Steps = append(plan.Steps, domain.RuntimeInstallStep{Type: step.Type, TargetKey: step.TargetKey, PackageManager: step.PackageManager, PackageName: step.PackageName, Version: step.Version, DownloadRef: step.DownloadRef, Checksum: step.Checksum})
}
profiles.InstallPlans = append(profiles.InstallPlans, plan)
}
for _, item := range body.LogSources {
profiles.LogSources = append(profiles.LogSources, domain.RuntimeLogSource{Key: item.Key, Kind: item.Kind, TargetKey: item.TargetKey, StreamKey: item.StreamKey, CursorKind: item.CursorKind, RetentionDays: item.RetentionDays})
}
for _, item := range body.TransportProfiles {
profiles.TransportProfiles = append(profiles.TransportProfiles, domain.RuntimeTransportProfile{Key: item.Key, Kind: item.Kind, TargetKey: item.TargetKey, Capabilities: domain.CopyStringSlice(item.Capabilities)})
}
for _, item := range body.DataTargets {
profiles.DataTargets = append(profiles.DataTargets, domain.RuntimeDataTarget{Key: item.Key, Kind: item.Kind, TransportKey: item.TransportKey, SourceRootKey: item.SourceRootKey, SourcePath: item.SourcePath, WorkspaceKey: item.WorkspaceKey, RefreshPolicy: item.RefreshPolicy, MaxBytes: item.MaxBytes, Platforms: domain.CopyStringSlice(item.Platforms)})
}
for _, item := range body.DLLExtensions {
extension := domain.RuntimeDLLExtensionProfile{Key: item.Key, DisplayName: item.DisplayName, Kind: item.Kind, Activation: item.Activation, Version: item.Version, ReleaseState: item.ReleaseState, ReleaseURL: item.ReleaseURL, Checksum: item.Checksum, SizeBytes: item.SizeBytes, TargetKey: item.TargetKey, ModKey: item.ModKey, DLLRef: item.DLLRef, TargetExecutableChecksum: item.TargetExecutableChecksum, UE4SSABI: item.UE4SSABI, UpdateOnStart: item.UpdateOnStart, RCONPort: item.RCONPort}
for _, target := range item.SupportedTargets {
extension.SupportedTargets = append(extension.SupportedTargets, domain.RuntimeTarget{OS: target.OS, Arch: target.Arch})
}
profiles.DLLExtensions = append(profiles.DLLExtensions, extension)
}
return profiles
}
func runtimeProfilesFromDomain(profiles domain.GamePluginRuntimeProfiles) GamePluginRuntimeProfilesResponseBody {
profiles = domain.CopyGamePluginRuntimeProfiles(profiles)
body := GamePluginRuntimeProfilesResponseBody{}
for _, item := range profiles.Discovery {
body.Discovery = append(body.Discovery, RuntimeDiscoveryProbeBody{Key: item.Key, Kind: item.Kind, TargetKey: item.TargetKey, Required: item.Required, Expected: item.Expected, Platforms: item.Platforms})
}
for _, item := range profiles.LifecycleProfiles {
body.LifecycleProfiles = append(body.LifecycleProfiles, RuntimeLifecycleProfileBody{Key: item.Key, Mode: item.Mode, Capabilities: item.Capabilities, ActionRefs: lifecycleActionsFromDomain(item.ActionRefs), TransportKeys: item.TransportKeys, DLLExtensionRefs: item.DLLExtensionRefs, Platforms: item.Platforms})
}
for _, item := range profiles.DependencyProbes {
body.DependencyProbes = append(body.DependencyProbes, RuntimeDependencyProbeBody{Key: item.Key, Kind: item.Kind, TargetKey: item.TargetKey, Required: item.Required, MinimumVersion: item.MinimumVersion, SteamAppID: item.SteamAppID, Platforms: item.Platforms})
}
for _, item := range profiles.InstallPlans {
plan := RuntimeInstallPlanBody{Key: item.Key, Title: item.Title, Platforms: item.Platforms}
for _, step := range item.Steps {
plan.Steps = append(plan.Steps, RuntimeInstallStepBody{Type: step.Type, TargetKey: step.TargetKey, PackageManager: step.PackageManager, PackageName: step.PackageName, Version: step.Version, DownloadRef: step.DownloadRef, Checksum: step.Checksum})
}
body.InstallPlans = append(body.InstallPlans, plan)
}
for _, item := range profiles.LogSources {
body.LogSources = append(body.LogSources, RuntimeLogSourceBody{Key: item.Key, Kind: item.Kind, TargetKey: item.TargetKey, StreamKey: item.StreamKey, CursorKind: item.CursorKind, RetentionDays: item.RetentionDays})
}
for _, item := range profiles.TransportProfiles {
body.TransportProfiles = append(body.TransportProfiles, RuntimeTransportProfileBody{Key: item.Key, Kind: item.Kind, TargetKey: item.TargetKey, Capabilities: item.Capabilities})
}
for _, item := range profiles.DataTargets {
body.DataTargets = append(body.DataTargets, RuntimeDataTargetBody{Key: item.Key, Kind: item.Kind, TransportKey: item.TransportKey, SourceRootKey: item.SourceRootKey, SourcePath: item.SourcePath, WorkspaceKey: item.WorkspaceKey, RefreshPolicy: item.RefreshPolicy, MaxBytes: item.MaxBytes, Platforms: item.Platforms})
}
for _, item := range profiles.DLLExtensions {
host, filename := safeDLLReleaseLocation(item.ReleaseURL)
extension := RuntimeDLLExtensionProfileResponseBody{Key: item.Key, DisplayName: item.DisplayName, Kind: item.Kind, Activation: item.Activation, Version: item.Version, ReleaseState: item.ReleaseState, ReleaseHost: host, ReleaseFilename: filename, Checksum: safeDLLChecksumPrefix(item.Checksum), SizeBytes: item.SizeBytes, TargetExecutableChecksum: safeDLLChecksumPrefix(item.TargetExecutableChecksum), UE4SSABI: item.UE4SSABI, UpdateOnStart: item.UpdateOnStart}
for _, target := range item.SupportedTargets {
extension.SupportedTargets = append(extension.SupportedTargets, RuntimeTargetBody{OS: target.OS, Arch: target.Arch})
}
body.DLLExtensions = append(body.DLLExtensions, extension)
}
return body
}
func dllExtensionPlansFromDomain(plans []domain.RuntimeDLLExtensionPlan) []RuntimeDLLExtensionPlanBody {
items := make([]RuntimeDLLExtensionPlanBody, 0, len(plans))
for _, item := range plans {
items = append(items, RuntimeDLLExtensionPlanBody{Key: item.Key, Version: item.Version, ReleaseURL: item.ReleaseURL, Checksum: item.Checksum, SizeBytes: item.SizeBytes, TargetKey: item.TargetKey, ModKey: item.ModKey, DLLRef: item.DLLRef, TargetExecutableChecksum: item.TargetExecutableChecksum, UE4SSABI: item.UE4SSABI, RCONPort: item.RCONPort})
}
return items
}
func safeDLLReleaseLocation(value string) (string, string) {
parsed, err := url.Parse(value)
if err != nil || parsed == nil {
return "", ""
}
return parsed.Hostname(), path.Base(parsed.Path)
}
func safeDLLChecksumPrefix(value string) string {
if len(value) <= len("sha256:")+12 {
return value
}
return value[:len("sha256:")+12] + "…"
}