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:
@@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -50,6 +51,17 @@ func TestRunHelperProcess(t *testing.T) {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
if pidText := os.Getenv("RUN_GRACEFUL_STOP_PID"); pidText != "" {
|
||||
// Stands in for a game-owned graceful shutdown: the plugin-declared
|
||||
// stop command asks the running game to exit instead of Run
|
||||
// terminating the supervised process.
|
||||
if target, convErr := strconv.Atoi(pidText); convErr == nil {
|
||||
if process, findErr := os.FindProcess(target); findErr == nil {
|
||||
_ = process.Kill()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
if os.Getenv("RUN_EXIT_NOW") == "1" {
|
||||
return
|
||||
}
|
||||
@@ -132,6 +144,106 @@ func TestTypedProcessOutputCaptureResumesAfterRunRestart(t *testing.T) {
|
||||
waitForManagedTailers(t, restarted.managed.(*OSManagedProcessSupervisor))
|
||||
}
|
||||
|
||||
func TestGracefulStopRunsDeclaredShutdownBeforeTermination(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
start := executionAssignment(protocol.RunCapabilityProcessStart)
|
||||
setupProcessWorkspace(t, root, start, false)
|
||||
scope := processScope(root, start)
|
||||
executor := NewLifecycleExecutor(WithLifecycleWorkspaceRoot(root), WithProcessLogSink(&recordingLogSink{}))
|
||||
if result := executor.Execute(start); result.State != lifecycleResultStateSucceeded {
|
||||
t.Fatalf("start managed process: %+v", result)
|
||||
}
|
||||
managed := executor.managed.(*OSManagedProcessSupervisor)
|
||||
identity := managed.Status(ProcessIdentity{Scope: scope})
|
||||
writeJSONFixture(t, filepath.Join(scope, "actions", "stop.json"), map[string]any{
|
||||
"version": 1,
|
||||
"action": "stop",
|
||||
"mode": "control",
|
||||
"stopTimeoutMs": 30000,
|
||||
"gracefulStop": map[string]any{
|
||||
"executableKey": "bin/game-server",
|
||||
"arguments": []string{"-test.run=TestRunHelperProcess"},
|
||||
"environment": map[string]string{"RUN_TEST_HELPER": "1", "RUN_GRACEFUL_STOP_PID": strconv.Itoa(identity.PID)},
|
||||
"timeoutMs": 30000,
|
||||
"fallback": "report",
|
||||
},
|
||||
})
|
||||
|
||||
stop := executionAssignment(protocol.RunCapabilityProcessStop)
|
||||
stop.TargetKey = "actions/stop.json"
|
||||
result := executor.Execute(stop)
|
||||
|
||||
if result.State != lifecycleResultStateSucceeded {
|
||||
t.Fatalf("graceful stop result: %+v", result)
|
||||
}
|
||||
if result.ExecutionResult.ProcessState != "stopped" {
|
||||
t.Fatalf("graceful stop process state: %+v", result.ExecutionResult)
|
||||
}
|
||||
if result.ExecutionResult.ExitClassification == "forced-stop" {
|
||||
t.Fatalf("declared graceful stop terminated the process: %+v", result.ExecutionResult)
|
||||
}
|
||||
if _, err := os.FindProcess(identity.PID); err == nil && processAliveForTest(identity.PID) {
|
||||
t.Fatalf("declared shutdown did not stop process %d", identity.PID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGracefulStopReportsWhenDeclaredShutdownDoesNotStopProcess(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
start := executionAssignment(protocol.RunCapabilityProcessStart)
|
||||
setupProcessWorkspace(t, root, start, false)
|
||||
scope := processScope(root, start)
|
||||
executor := NewLifecycleExecutor(WithLifecycleWorkspaceRoot(root), WithProcessLogSink(&recordingLogSink{}))
|
||||
if result := executor.Execute(start); result.State != lifecycleResultStateSucceeded {
|
||||
t.Fatalf("start managed process: %+v", result)
|
||||
}
|
||||
managed := executor.managed.(*OSManagedProcessSupervisor)
|
||||
identity := managed.Status(ProcessIdentity{Scope: scope})
|
||||
writeStopAction := func(fallback string) {
|
||||
writeJSONFixture(t, filepath.Join(scope, "actions", "stop.json"), map[string]any{
|
||||
"version": 1,
|
||||
"action": "stop",
|
||||
"mode": "control",
|
||||
"stopTimeoutMs": 30000,
|
||||
"gracefulStop": map[string]any{
|
||||
"executableKey": "bin/game-server",
|
||||
"arguments": []string{"-test.run=TestRunHelperProcess"},
|
||||
"environment": map[string]string{"RUN_TEST_HELPER": "1", "RUN_LOG_LINES": "1"},
|
||||
"timeoutMs": 30000,
|
||||
"fallback": fallback,
|
||||
},
|
||||
})
|
||||
}
|
||||
stop := executionAssignment(protocol.RunCapabilityProcessStop)
|
||||
stop.TargetKey = "actions/stop.json"
|
||||
t.Cleanup(func() {
|
||||
writeStopAction("terminate")
|
||||
_ = executor.Execute(stop)
|
||||
})
|
||||
|
||||
writeStopAction("report")
|
||||
reported := executor.Execute(stop)
|
||||
if reported.State != lifecycleResultStateFailed || reported.ErrorCode != "graceful_stop_timeout" {
|
||||
t.Fatalf("expected unverified graceful stop failure, got %+v", reported)
|
||||
}
|
||||
if current := managed.Status(ProcessIdentity{Scope: scope}); current.State != "running" {
|
||||
t.Fatalf("report-only graceful stop terminated the process: %+v", current)
|
||||
}
|
||||
|
||||
writeStopAction("terminate")
|
||||
terminated := executor.Execute(stop)
|
||||
if terminated.State != lifecycleResultStateSucceeded || terminated.ExecutionResult.ProcessState != "stopped" {
|
||||
t.Fatalf("expected declared fallback termination, got %+v", terminated)
|
||||
}
|
||||
if current := managed.Status(ProcessIdentity{Scope: scope}); current.State == "running" {
|
||||
t.Fatalf("declared fallback termination left the process running: %+v", current)
|
||||
}
|
||||
_ = identity
|
||||
}
|
||||
|
||||
func processAliveForTest(pid int) bool {
|
||||
return processAlivePID(pid)
|
||||
}
|
||||
|
||||
func TestManagedProcessOutputAfterCanceledRunContextIsNotSpooledOnRestart(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
assignment := executionAssignment(protocol.RunCapabilityProcessStart)
|
||||
|
||||
Reference in New Issue
Block a user