Support declared graceful stop and steam.update probe
- LifecycleActionTemplate 新增 gracefulStop:stop 动作可声明插件自有的优雅关闭命令、 参数、环境、超时与 fallback;超时且 fallback=report 时任务失败,避免默认杀进程。 - 依赖探针新增 steam.update:通过 steamcmd +app_info_print 读取公开分支 buildid, 与本地 appmanifest_<appid>.acf 比较,输出 installed/latest/update=yes|no|unknown。 - 依赖执行输入新增 ServerRoot,供探针定位插件声明的服务器安装目录。
This commit is contained in:
+134
-20
@@ -534,18 +534,32 @@ func (executor LifecycleExecutor) writeProcessLogs(ctx context.Context, assignme
|
||||
}
|
||||
|
||||
type LifecycleActionTemplate struct {
|
||||
Version int `json:"version,omitempty"`
|
||||
Action string `json:"action,omitempty"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
ExecutableKey string `json:"executableKey,omitempty"`
|
||||
TargetExecutableKey string `json:"targetExecutableKey,omitempty"`
|
||||
Arguments []string `json:"arguments,omitempty"`
|
||||
Environment map[string]string `json:"environment,omitempty"`
|
||||
OutputMode string `json:"outputMode,omitempty"`
|
||||
StopTimeoutMS int `json:"stopTimeoutMs,omitempty"`
|
||||
Command []string `json:"command"`
|
||||
Env map[string]string `json:"env,omitempty"`
|
||||
TimeoutMS int `json:"timeoutMs,omitempty"`
|
||||
Version int `json:"version,omitempty"`
|
||||
Action string `json:"action,omitempty"`
|
||||
Mode string `json:"mode,omitempty"`
|
||||
ExecutableKey string `json:"executableKey,omitempty"`
|
||||
TargetExecutableKey string `json:"targetExecutableKey,omitempty"`
|
||||
Arguments []string `json:"arguments,omitempty"`
|
||||
Environment map[string]string `json:"environment,omitempty"`
|
||||
OutputMode string `json:"outputMode,omitempty"`
|
||||
StopTimeoutMS int `json:"stopTimeoutMs,omitempty"`
|
||||
GracefulStop *GracefulStopActionTemplate `json:"gracefulStop,omitempty"`
|
||||
Command []string `json:"command"`
|
||||
Env map[string]string `json:"env,omitempty"`
|
||||
TimeoutMS int `json:"timeoutMs,omitempty"`
|
||||
}
|
||||
|
||||
// GracefulStopActionTemplate declares the plugin-owned shutdown step that runs
|
||||
// before a supervised process is terminated. The command is ordinary
|
||||
// plugin-declared game policy: it can announce a shutdown, ask the game to
|
||||
// save, and wait for the game to exit on its own. Run only reports and keeps
|
||||
// the bounded wait here; it never inspects the command text or its output.
|
||||
type GracefulStopActionTemplate struct {
|
||||
ExecutableKey string `json:"executableKey,omitempty"`
|
||||
Arguments []string `json:"arguments,omitempty"`
|
||||
Environment map[string]string `json:"environment,omitempty"`
|
||||
TimeoutMS int `json:"timeoutMs,omitempty"`
|
||||
Fallback string `json:"fallback,omitempty"`
|
||||
}
|
||||
|
||||
func (executor LifecycleExecutor) loadLifecycleTemplate(assignment protocol.RunJobAssignment) (LifecycleActionTemplate, string, error) {
|
||||
@@ -608,14 +622,7 @@ func (executor LifecycleExecutor) executeManaged(ctx context.Context, assignment
|
||||
return processExecutionResult(item, "process started")
|
||||
}
|
||||
if assignment.Capability == protocol.RunCapabilityProcessStop {
|
||||
log.Printf("RUN phase=lifecycle.managed status=stopping job=%s scope=%s", assignment.JobID, scope)
|
||||
item, err := executor.managed.Stop(ctx, identity)
|
||||
if err != nil {
|
||||
log.Printf("RUN phase=lifecycle.managed status=stop_failed job=%s error=%s", assignment.JobID, err.Error())
|
||||
return lifecycleFailure("lifecycle_stop_failed", err.Error())
|
||||
}
|
||||
log.Printf("RUN phase=lifecycle.managed status=stopped job=%s pid=%d state=%s classification=%s", assignment.JobID, item.PID, item.State, safeOptional(item.ExitClassification))
|
||||
return processExecutionResult(item, "process stopped")
|
||||
return executor.stopManaged(ctx, assignment, template, scope, identity)
|
||||
}
|
||||
log.Printf("RUN phase=lifecycle.managed status=querying job=%s scope=%s", assignment.JobID, scope)
|
||||
item := executor.managed.Status(identity)
|
||||
@@ -623,6 +630,113 @@ func (executor LifecycleExecutor) executeManaged(ctx context.Context, assignment
|
||||
return processExecutionResult(item, "process status queried")
|
||||
}
|
||||
|
||||
// stopManaged executes the plugin-declared shutdown step for a supervised
|
||||
// process, then applies the plugin-declared fallback policy. Run keeps the
|
||||
// generic supervision rules here; the declared command owns all game policy
|
||||
// (player announcement, save request, shutdown command, exit polling).
|
||||
func (executor LifecycleExecutor) stopManaged(ctx context.Context, assignment protocol.RunJobAssignment, template LifecycleActionTemplate, scope string, identity ProcessIdentity) LifecycleExecutionResult {
|
||||
declared := template.GracefulStop
|
||||
if declared == nil || strings.TrimSpace(declared.ExecutableKey) == "" {
|
||||
log.Printf("RUN phase=lifecycle.managed status=stopping job=%s scope=%s", assignment.JobID, scope)
|
||||
return executor.terminateManagedProcess(ctx, assignment, identity)
|
||||
}
|
||||
fallback := strings.TrimSpace(declared.Fallback)
|
||||
if fallback == "" {
|
||||
fallback = "report"
|
||||
}
|
||||
if fallback != "report" && fallback != "terminate" {
|
||||
log.Printf("RUN phase=lifecycle.managed status=graceful_stop_invalid job=%s fallback=%s", assignment.JobID, fallback)
|
||||
return lifecycleFailure("unsafe_lifecycle_command", "graceful stop fallback policy is unsupported")
|
||||
}
|
||||
command, err := declared.toProcessCommand(template, executor.workspaceRoot, scope, assignment)
|
||||
if err != nil {
|
||||
log.Printf("RUN phase=lifecycle.managed status=graceful_stop_invalid job=%s error=%s", assignment.JobID, err.Error())
|
||||
return lifecycleFailure("unsafe_graceful_stop_command", err.Error())
|
||||
}
|
||||
log.Printf("RUN phase=lifecycle.managed status=graceful_stop_requested job=%s scope=%s fallback=%s command=%s timeoutMs=%d", assignment.JobID, scope, fallback, quotedCommandLine(command.Args), command.Timeout.Milliseconds())
|
||||
command.OutputLine = func(stream string, line string) {
|
||||
_ = executor.logSink.Append(ctx, assignment, stream, line)
|
||||
}
|
||||
result, err := executor.supervisor.Run(ctx, command)
|
||||
if !result.OutputRelayed {
|
||||
executor.writeProcessLogs(ctx, assignment, result)
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
log.Printf("RUN phase=lifecycle.managed status=graceful_stop_cancelled job=%s error=%s", assignment.JobID, ctx.Err().Error())
|
||||
return lifecycleExecutionFailure("lifecycle_cancelled", "lifecycle action cancelled", false)
|
||||
}
|
||||
if err != nil || result.ExitCode != 0 {
|
||||
log.Printf("RUN phase=lifecycle.managed status=graceful_stop_failed job=%s exitCode=%d error=%s", assignment.JobID, result.ExitCode, errorSummary(err))
|
||||
return executor.gracefulStopFallback(ctx, assignment, identity, fallback, "graceful_stop_failed")
|
||||
}
|
||||
current := executor.managed.Status(identity)
|
||||
if current.State != "running" {
|
||||
item, failure := executor.terminateManagedProcessResult(ctx, assignment, identity)
|
||||
if failure.State != "" {
|
||||
return failure
|
||||
}
|
||||
log.Printf("RUN phase=lifecycle.managed status=graceful_stop_completed job=%s pid=%d state=%s classification=%s", assignment.JobID, item.PID, item.State, safeOptional(item.ExitClassification))
|
||||
result := processExecutionResult(item, "process stopped gracefully")
|
||||
result.Progress.Message = "process stopped gracefully"
|
||||
return result
|
||||
}
|
||||
log.Printf("RUN phase=lifecycle.managed status=graceful_stop_alive job=%s pid=%d fallback=%s", assignment.JobID, current.PID, fallback)
|
||||
return executor.gracefulStopFallback(ctx, assignment, identity, fallback, "graceful_stop_timeout")
|
||||
}
|
||||
|
||||
func (executor LifecycleExecutor) gracefulStopFallback(ctx context.Context, assignment protocol.RunJobAssignment, identity ProcessIdentity, fallback string, failureCode string) LifecycleExecutionResult {
|
||||
if fallback != "terminate" {
|
||||
log.Printf("RUN phase=lifecycle.managed status=graceful_stop_unverified job=%s code=%s", assignment.JobID, failureCode)
|
||||
return lifecycleFailure(failureCode, "declared graceful stop did not stop the process and the plugin does not allow termination")
|
||||
}
|
||||
log.Printf("RUN phase=lifecycle.managed status=graceful_stop_escalated job=%s code=%s", assignment.JobID, failureCode)
|
||||
return executor.terminateManagedProcess(ctx, assignment, identity)
|
||||
}
|
||||
|
||||
func (executor LifecycleExecutor) terminateManagedProcessResult(ctx context.Context, assignment protocol.RunJobAssignment, identity ProcessIdentity) (ProcessIdentity, LifecycleExecutionResult) {
|
||||
item, err := executor.managed.Stop(ctx, identity)
|
||||
if err != nil {
|
||||
log.Printf("RUN phase=lifecycle.managed status=stop_failed job=%s error=%s", assignment.JobID, err.Error())
|
||||
return ProcessIdentity{}, lifecycleFailure("lifecycle_stop_failed", err.Error())
|
||||
}
|
||||
log.Printf("RUN phase=lifecycle.managed status=stopped job=%s pid=%d state=%s classification=%s", assignment.JobID, item.PID, item.State, safeOptional(item.ExitClassification))
|
||||
return item, LifecycleExecutionResult{}
|
||||
}
|
||||
|
||||
func (executor LifecycleExecutor) terminateManagedProcess(ctx context.Context, assignment protocol.RunJobAssignment, identity ProcessIdentity) LifecycleExecutionResult {
|
||||
item, failure := executor.terminateManagedProcessResult(ctx, assignment, identity)
|
||||
if item.State == "" && failure.State != "" {
|
||||
return failure
|
||||
}
|
||||
return processExecutionResult(item, "process stopped")
|
||||
}
|
||||
|
||||
func (declaration GracefulStopActionTemplate) toProcessCommand(template LifecycleActionTemplate, workspaceRoot string, scope string, assignment protocol.RunJobAssignment) (ProcessCommand, error) {
|
||||
stopTemplate := LifecycleActionTemplate{
|
||||
Action: template.Action,
|
||||
ExecutableKey: declaration.ExecutableKey,
|
||||
Arguments: declaration.Arguments,
|
||||
Environment: mergeLifecycleEnvironment(template.Environment, declaration.Environment),
|
||||
OutputMode: "pipes",
|
||||
TimeoutMS: declaration.TimeoutMS,
|
||||
}
|
||||
return stopTemplate.ToExecutableProcessCommand(NewWorkspaceResolver(workspaceRoot), scope, assignment)
|
||||
}
|
||||
|
||||
func mergeLifecycleEnvironment(base map[string]string, override map[string]string) map[string]string {
|
||||
if len(base) == 0 && len(override) == 0 {
|
||||
return nil
|
||||
}
|
||||
merged := make(map[string]string, len(base)+len(override))
|
||||
for key, value := range base {
|
||||
merged[key] = value
|
||||
}
|
||||
for key, value := range override {
|
||||
merged[key] = value
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
func (executor LifecycleExecutor) ResumeManagedProcessLogs(ctx context.Context) {
|
||||
if executor.managed == nil {
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user