Files
browser/platform/domain/job_channel.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

513 lines
15 KiB
Go

package domain
import "time"
type RunJobProgressReport struct {
Percent int
Phase string
Message string
}
type RunJobAssignment struct {
JobID string
ServerInstanceID string
RunEndpointID string
Capability string
TargetKey string
InputRef string
IdempotencyKey string
State JobState
Progress RunJobProgressReport
ResultRef string
ExecutionInput JobExecutionInput
LeaseToken string
Attempt int
FencingToken uint64
MaxAttempts int
AckDeadlineAt time.Time
LeaseExpiresAt time.Time
NextAttemptAt time.Time
ProgressSequence uint64
CreatedAt time.Time
UpdatedAt time.Time
}
type RunJobClaim struct {
RunEndpointID string
SessionToken string
Capabilities []string
Capacity RunCapacity
WaitSeconds int
}
type RunJobClaimResult struct {
Accepted bool
RunEndpointID string
HasJob bool
Job *RunJobAssignment
NextPollSeconds int
ServerTime time.Time
}
type RunJobAck struct {
RunEndpointID string
SessionToken string
JobID string
LeaseToken string
Attempt int
Message string
}
type RunJobAckResult struct {
Accepted bool
Job RunJobAssignment
ServerTime time.Time
}
type RunJobProgress struct {
RunEndpointID string
SessionToken string
JobID string
LeaseToken string
Attempt int
Progress RunJobProgressReport
Sequence uint64
}
type RunJobProgressResult struct {
Accepted bool
Job RunJobAssignment
ServerTime time.Time
}
type RunJobResult struct {
RunEndpointID string
SessionToken string
JobID string
LeaseToken string
Attempt int
State JobState
Progress RunJobProgressReport
ResultRef string
Message string
ErrorCode string
Retryable bool
ExecutionResult JobExecutionResult
}
type RunJobResultResult struct {
Accepted bool
Job RunJobAssignment
ServerTime time.Time
}
type DistributionBuildInputRequest struct {
RunEndpointID string
SessionToken string
JobID string
LeaseToken string
Attempt int
}
type DistributionBuildInput struct {
JobID string
ComponentKind DistributionComponentKind
ServerInstanceID string
PluginID string
RunEndpointID string
ProfileKey string
TargetOS string
TargetArch string
TargetRelease string
PlatformURL string
PackageFormat string
RepositoryURL string
SourceRevision string
ArtifactID string
OutputFilename string
SecretRef string
KeyGeneration int
AuthKey string
WorkspaceSeed string
AutonomousLifecycle *RunAutonomousLifecyclePlan
}
type RunAutonomousLifecyclePlan struct {
SchemaVersion string `json:"schemaVersion"`
ServerInstanceID string `json:"serverInstanceId"`
PluginID string `json:"pluginId"`
PluginVersion string `json:"pluginVersion"`
RunEndpointID string `json:"runEndpointId"`
ProfileKey string `json:"profileKey,omitempty"`
TargetOS string `json:"targetOs"`
TargetArch string `json:"targetArch"`
TargetRelease string `json:"targetRelease"`
DeploymentRevision int `json:"deploymentRevision,omitempty"`
Bootstrap *RunAutonomousLifecycleAction `json:"bootstrap,omitempty"`
Actions []RunAutonomousLifecycleAction `json:"actions,omitempty"`
DependencyProbes []RunAutonomousDependencyProbe `json:"dependencyProbes,omitempty"`
InstallPlans []RunAutonomousInstallPlan `json:"installPlans,omitempty"`
LogSources []RunAutonomousLogSource `json:"logSources,omitempty"`
DLLExtensions []RunAutonomousDLLExtension `json:"dllExtensions,omitempty"`
DataTargets []RunAutonomousDataTarget `json:"dataTargets,omitempty"`
RuntimeBindings map[string]string `json:"runtimeBindings,omitempty"`
Deployment *RunAutonomousDeployment `json:"deployment,omitempty"`
}
type RunAutonomousLifecycleAction struct {
Action ServerLifecycleAction `json:"action"`
Operation string `json:"operation"`
Capability string `json:"capability"`
TargetKey string `json:"targetKey"`
}
type RunAutonomousDependencyProbe 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 RunAutonomousInstallPlan struct {
Key string `json:"key"`
Title string `json:"title,omitempty"`
Platforms []string `json:"platforms,omitempty"`
Steps []RunAutonomousInstallStep `json:"steps,omitempty"`
}
type RunAutonomousInstallStep 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 RunAutonomousLogSource struct {
Key string `json:"key"`
Kind string `json:"kind"`
TargetKey string `json:"targetKey"`
StreamKey string `json:"streamKey,omitempty"`
CursorKind string `json:"cursorKind,omitempty"`
RetentionDays int `json:"retentionDays,omitempty"`
}
type RunAutonomousDLLExtension struct {
Key string `json:"key"`
Version string `json:"version"`
ReleaseURL string `json:"releaseUrl"`
Checksum string `json:"checksum"`
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"`
RCONPort int `json:"rconPort,omitempty"`
}
// RunAutonomousDataTarget is a package-local snapshot declaration. The source
// root is resolved only inside Run from a private generated package binding.
type RunAutonomousDataTarget 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,omitempty"`
Platforms []string `json:"platforms,omitempty"`
}
type RunAutonomousDeployment struct {
SchemaVersion string `json:"schemaVersion"`
Mode ServerDeploymentMode `json:"mode"`
ProfileKey string `json:"profileKey,omitempty"`
RuntimeBindings map[string]string `json:"runtimeBindings,omitempty"`
CreateInputs map[string]string `json:"createInputs,omitempty"`
ServerRoot string `json:"serverRoot,omitempty"`
WorkingDirectory string `json:"workingDirectory,omitempty"`
InstallCommand string `json:"installCommand,omitempty"`
StartCommand string `json:"startCommand,omitempty"`
StopCommand string `json:"stopCommand,omitempty"`
StatusCommand string `json:"statusCommand,omitempty"`
Shell ServerCommandShell `json:"shell,omitempty"`
Revision int `json:"revision,omitempty"`
}
type DependencyExecutionInputRequest struct {
RunEndpointID string
SessionToken string
JobID string
LeaseToken string
Attempt int
}
type DependencyExecutionInput struct {
JobID string
ServerInstanceID string
RunEndpointID string
PluginID string
PluginVersion string
ProfileKey string
TargetOS string
TargetArch string
PlanDigest string
ServerRoot string
Probe RuntimeDependencyProbe
Plan RuntimeInstallPlan
Bindings map[string]string
}
type SourceRCONExecutionInputRequest struct {
RunEndpointID string
SessionToken string
JobID string
LeaseToken string
Attempt int
}
// SourceRCONExecutionInput is sent only to the active signed Run lease. Its
// Command value must never be persisted in a Job, assignment, or journal.
type SourceRCONExecutionInput struct {
JobID string
ServerInstanceID string
RunEndpointID string
Command string
}
type RunUpdateInputRequest struct {
RunEndpointID string
SessionToken string
JobID string
LeaseToken string
Attempt int
}
type RunUpdateInput struct {
JobID string
ServerInstanceID string
RunEndpointID string
ArtifactID string
Checksum string
SizeBytes int64
TargetOS string
TargetArch string
PackageFormat string
ExecutableName string
TargetRelease string
ChunkSizeBytes int
}
type RunUpdateChunkRequest struct {
RunEndpointID string
SessionToken string
JobID string
LeaseToken string
Attempt int
Offset int64
Length int
}
type RunUpdateChunk struct {
JobID string
ArtifactID string
Offset int64
TotalBytes int64
Checksum string
Payload []byte
Complete bool
}
type RunUpdateHealthReport struct {
RunEndpointID string
SessionToken string
JobID string
LeaseToken string
Attempt int
Outcome string
Version string
}
type RunUpdateHealthResult struct {
Accepted bool
JobID string
Phase RunUpdatePhase
ServerTime time.Time
}
type DependencyExecutionEvidence struct {
ProbeKey string `json:"probeKey"`
PlanKey string `json:"planKey,omitempty"`
PlanDigest string `json:"planDigest"`
State string `json:"state"`
Evidence string `json:"evidence,omitempty"`
CompletedSteps int `json:"completedSteps,omitempty"`
}
type RunUpdateExecutionEvidence struct {
TargetRelease string `json:"targetRelease"`
Phase string `json:"phase"`
}
type RunJobCancelRequest struct {
JobID string
Reason string
}
type RunJobCancelRequestResult struct {
Accepted bool
JobID string
Reason string
RequestedAt time.Time
CompletedAt time.Time
State JobState
}
type RunJobCancelPoll struct {
RunEndpointID string
SessionToken string
JobID string
LeaseToken string
Attempt int
}
type RunJobCancelPollResult struct {
Accepted bool
RunEndpointID string
HasCancel bool
JobID string
Reason string
RequestedAt time.Time
ServerTime time.Time
}
type RunJobReconcileEntry struct {
JobID string
LeaseToken string
Attempt int
}
type RunJobReconcile struct {
RunEndpointID string
SessionToken string
ActiveJobs []RunJobReconcileEntry
}
type RunJobReconcileResult struct {
Accepted bool
RunEndpointID string
ConfirmedJobs []RunJobAssignment
DiscardJobIDs []string
ServerTime time.Time
}
func CopyRunJobAssignment(assignment RunJobAssignment) RunJobAssignment {
assignment.ExecutionInput.Inputs = CopyStringMap(assignment.ExecutionInput.Inputs)
assignment.ExecutionInput.LogSource = CopyRuntimeLogSourcePtr(assignment.ExecutionInput.LogSource)
assignment.ExecutionInput.LogSources = CopyRuntimeLogSources(assignment.ExecutionInput.LogSources)
assignment.ExecutionInput.DLLExtensions = append([]RuntimeDLLExtensionPlan(nil), assignment.ExecutionInput.DLLExtensions...)
assignment.ExecutionInput.SourceRCON = CopyRuntimeSourceRCONPlan(assignment.ExecutionInput.SourceRCON)
return assignment
}
func CopyRunJobAssignmentPtr(assignment *RunJobAssignment) *RunJobAssignment {
if assignment == nil {
return nil
}
copy := CopyRunJobAssignment(*assignment)
return &copy
}
func CopyRunJobClaim(claim RunJobClaim) RunJobClaim {
claim.Capabilities = CopyStringSlice(claim.Capabilities)
return claim
}
func CopyRunJobClaimResult(result RunJobClaimResult) RunJobClaimResult {
result.Job = CopyRunJobAssignmentPtr(result.Job)
return result
}
func CopyRunJobReconcile(reconcile RunJobReconcile) RunJobReconcile {
if reconcile.ActiveJobs != nil {
reconcile.ActiveJobs = append([]RunJobReconcileEntry(nil), reconcile.ActiveJobs...)
}
return reconcile
}
func CopyRunJobReconcileResult(result RunJobReconcileResult) RunJobReconcileResult {
result.ConfirmedJobs = CopyRunJobAssignments(result.ConfirmedJobs)
result.DiscardJobIDs = CopyStringSlice(result.DiscardJobIDs)
return result
}
func CopyRunJobAssignments(assignments []RunJobAssignment) []RunJobAssignment {
if assignments == nil {
return nil
}
out := make([]RunJobAssignment, len(assignments))
for index, assignment := range assignments {
out[index] = CopyRunJobAssignment(assignment)
}
return out
}
func CopyDependencyExecutionInput(input DependencyExecutionInput) DependencyExecutionInput {
input.Probe.Platforms = CopyStringSlice(input.Probe.Platforms)
input.Plan.Platforms = CopyStringSlice(input.Plan.Platforms)
input.Plan.Steps = append([]RuntimeInstallStep(nil), input.Plan.Steps...)
input.Bindings = CopyStringMap(input.Bindings)
return input
}
func CopyRunAutonomousLifecyclePlanPtr(plan *RunAutonomousLifecyclePlan) *RunAutonomousLifecyclePlan {
if plan == nil {
return nil
}
copy := *plan
if plan.Bootstrap != nil {
bootstrap := *plan.Bootstrap
copy.Bootstrap = &bootstrap
}
copy.Actions = append([]RunAutonomousLifecycleAction(nil), plan.Actions...)
copy.DependencyProbes = append([]RunAutonomousDependencyProbe(nil), plan.DependencyProbes...)
for i := range copy.DependencyProbes {
copy.DependencyProbes[i].Platforms = CopyStringSlice(plan.DependencyProbes[i].Platforms)
}
copy.InstallPlans = append([]RunAutonomousInstallPlan(nil), plan.InstallPlans...)
for i := range copy.InstallPlans {
copy.InstallPlans[i].Platforms = CopyStringSlice(plan.InstallPlans[i].Platforms)
copy.InstallPlans[i].Steps = append([]RunAutonomousInstallStep(nil), plan.InstallPlans[i].Steps...)
}
copy.LogSources = append([]RunAutonomousLogSource(nil), plan.LogSources...)
copy.DLLExtensions = append([]RunAutonomousDLLExtension(nil), plan.DLLExtensions...)
copy.DataTargets = append([]RunAutonomousDataTarget(nil), plan.DataTargets...)
for i := range copy.DataTargets {
copy.DataTargets[i].Platforms = CopyStringSlice(plan.DataTargets[i].Platforms)
}
copy.RuntimeBindings = CopyStringMap(plan.RuntimeBindings)
if plan.Deployment != nil {
deployment := *plan.Deployment
deployment.RuntimeBindings = CopyStringMap(plan.Deployment.RuntimeBindings)
deployment.CreateInputs = CopyStringMap(plan.Deployment.CreateInputs)
copy.Deployment = &deployment
}
return &copy
}
func CopySourceRCONExecutionInput(input SourceRCONExecutionInput) SourceRCONExecutionInput {
return input
}
func CopyRunUpdateChunk(chunk RunUpdateChunk) RunUpdateChunk {
chunk.Payload = append([]byte(nil), chunk.Payload...)
return chunk
}