Remove legacy steamcmd and SCUM runtime coupling
This commit is contained in:
@@ -3,7 +3,7 @@ package main
|
|||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
func TestDiagnosticPlatformAddressOmitsCredentials(t *testing.T) {
|
func TestDiagnosticPlatformAddressOmitsCredentials(t *testing.T) {
|
||||||
if got := diagnosticPlatformAddress("https://token:secret@scum.npc0.com/api"); got != "https://scum.npc0.com/api" {
|
if got := diagnosticPlatformAddress("https://token:secret@platform.example.test/api"); got != "https://platform.example.test/api" {
|
||||||
t.Fatalf("unexpected diagnostic address %q", got)
|
t.Fatalf("unexpected diagnostic address %q", got)
|
||||||
}
|
}
|
||||||
if got := diagnosticPlatformAddress("not a URL"); got != "invalid" {
|
if got := diagnosticPlatformAddress("not a URL"); got != "invalid" {
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ func TestLoadUsesPackagedIdentityOverEnvironment(t *testing.T) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
BuildMode = "worker"
|
BuildMode = "worker"
|
||||||
BuildPlatformURL = "https://scum.npc0.com"
|
BuildPlatformURL = "https://platform.example.test"
|
||||||
BuildRunEndpointID = "run-server-1"
|
BuildRunEndpointID = "run-server-1"
|
||||||
BuildDisplayName = "Run-server-1"
|
BuildDisplayName = "Run-server-1"
|
||||||
BuildRegistrationToken = "compiled-run-key"
|
BuildRegistrationToken = "compiled-run-key"
|
||||||
@@ -80,7 +80,7 @@ func TestLoadUsesPackagedIdentityOverEnvironment(t *testing.T) {
|
|||||||
t.Setenv(key, "")
|
t.Setenv(key, "")
|
||||||
}
|
}
|
||||||
cfg := Load()
|
cfg := Load()
|
||||||
if cfg.Mode != "worker" || cfg.PlatformURL != "https://scum.npc0.com" || cfg.RunEndpointID != "run-server-1" || cfg.RegistrationToken != "compiled-run-key" || cfg.ServerInstanceID != "server-1" || cfg.PluginID != "game.scum" || cfg.ComponentKind != "run" || cfg.KeyGeneration != 5 || cfg.Version != "run-dist-1" || cfg.WorkspaceSeed != "seed-payload" {
|
if cfg.Mode != "worker" || cfg.PlatformURL != "https://platform.example.test" || cfg.RunEndpointID != "run-server-1" || cfg.RegistrationToken != "compiled-run-key" || cfg.ServerInstanceID != "server-1" || cfg.PluginID != "game.scum" || cfg.ComponentKind != "run" || cfg.KeyGeneration != 5 || cfg.Version != "run-dist-1" || cfg.WorkspaceSeed != "seed-payload" {
|
||||||
t.Fatalf("expected compiled defaults, got %+v", cfg)
|
t.Fatalf("expected compiled defaults, got %+v", cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ func ValidateRunAutonomousLifecyclePlan(plan RunAutonomousLifecyclePlan) error {
|
|||||||
return ValidationError("autonomous install plan is invalid")
|
return ValidationError("autonomous install plan is invalid")
|
||||||
}
|
}
|
||||||
for _, step := range installPlan.Steps {
|
for _, step := range installPlan.Steps {
|
||||||
if !ValidLogicalFileKey(step.TargetKey) || !validAutonomousToken(step.Type, 80) || step.PackageManager != "" && !validAutonomousToken(step.PackageManager, 80) || step.PackageName != "" && !validAutonomousToken(step.PackageName, 160) || step.Version != "" && !validAutonomousToken(step.Version, 120) {
|
if !validAutonomousInstallStepType(step.Type) || !ValidLogicalFileKey(step.TargetKey) || step.PackageManager != "" && !validAutonomousToken(step.PackageManager, 80) || step.PackageName != "" && !validAutonomousToken(step.PackageName, 160) || step.Version != "" && !validAutonomousToken(step.Version, 120) {
|
||||||
return ValidationError("autonomous install step is invalid")
|
return ValidationError("autonomous install step is invalid")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -139,6 +139,15 @@ func ValidateRunAutonomousLifecyclePlan(plan RunAutonomousLifecyclePlan) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validAutonomousInstallStepType(value string) bool {
|
||||||
|
switch value {
|
||||||
|
case "package", "verified-download", "manual":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func validateAutonomousDataTarget(target RunAutonomousDataTarget) error {
|
func validateAutonomousDataTarget(target RunAutonomousDataTarget) error {
|
||||||
if !ValidLogicalFileKey(target.Key) || !ValidLogicalFileKey(target.TransportKey) || !ValidLogicalFileKey(target.SourceRootKey) || !ValidLogicalFileKey(target.SourcePath) || !ValidLogicalFileKey(target.WorkspaceKey) {
|
if !ValidLogicalFileKey(target.Key) || !ValidLogicalFileKey(target.TransportKey) || !ValidLogicalFileKey(target.SourceRootKey) || !ValidLogicalFileKey(target.SourcePath) || !ValidLogicalFileKey(target.WorkspaceKey) {
|
||||||
return ValidationError("autonomous data target is invalid")
|
return ValidationError("autonomous data target is invalid")
|
||||||
|
|||||||
@@ -34,6 +34,15 @@ func TestValidateRunAutonomousLifecyclePlanRejectsUnsafeDataTargets(t *testing.T
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestValidateRunAutonomousLifecyclePlanRejectsLegacySteamCMDAppStep(t *testing.T) {
|
||||||
|
plan := validAutonomousLifecyclePlanForTest()
|
||||||
|
plan.InstallPlans = []DependencyInstallPlan{{Key: "install-game", Title: "Install game", Steps: []DependencyInstallStep{{Type: "steamcmd-app", TargetKey: "server/install-root", PackageManager: "steamcmd", PackageName: "123456"}}}}
|
||||||
|
|
||||||
|
if err := ValidateRunAutonomousLifecyclePlan(plan); err == nil {
|
||||||
|
t.Fatal("expected legacy steamcmd-app install step to be rejected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func validAutonomousLifecyclePlanForTest() RunAutonomousLifecyclePlan {
|
func validAutonomousLifecyclePlanForTest() RunAutonomousLifecyclePlan {
|
||||||
return RunAutonomousLifecyclePlan{SchemaVersion: "1", ServerInstanceID: "server-1", PluginID: "game.example", PluginVersion: "1.0.0", RunEndpointID: "run-1", ProfileKey: "run-local", TargetOS: "windows", TargetArch: "amd64", TargetRelease: "release-1", Bootstrap: &RunAutonomousLifecycleAction{Action: "start", Operation: "start", Capability: RunCapabilityProcessStart, TargetKey: "actions/start.json"}}
|
return RunAutonomousLifecyclePlan{SchemaVersion: "1", ServerInstanceID: "server-1", PluginID: "game.example", PluginVersion: "1.0.0", RunEndpointID: "run-1", ProfileKey: "run-local", TargetOS: "windows", TargetArch: "amd64", TargetRelease: "release-1", Bootstrap: &RunAutonomousLifecycleAction{Action: "start", Operation: "start", Capability: RunCapabilityProcessStart, TargetKey: "actions/start.json"}}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -237,7 +237,7 @@ type RuntimeDLLExtensionPlan struct {
|
|||||||
TargetKey string `json:"targetKey"`
|
TargetKey string `json:"targetKey"`
|
||||||
ModKey string `json:"modKey"`
|
ModKey string `json:"modKey"`
|
||||||
DLLRef string `json:"dllRef"`
|
DLLRef string `json:"dllRef"`
|
||||||
SCUMExecutableChecksum string `json:"scumExecutableChecksum"`
|
TargetExecutableChecksum string `json:"targetExecutableChecksum"`
|
||||||
UE4SSABI string `json:"ue4ssAbi"`
|
UE4SSABI string `json:"ue4ssAbi"`
|
||||||
RCONPort int `json:"rconPort"`
|
RCONPort int `json:"rconPort"`
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -59,7 +59,7 @@ Run distribution and runtime support jobs use the same lightweight job lifecycle
|
|||||||
|
|
||||||
- `run.self-update`: downloads an approved same-server target-matched distribution in bounded resumable ranges, verifies the final checksum, safely extracts exactly the expected executable, preserves config, and reports a rollback-safe staged result. A helper activates only after result acceptance, then waits for health and restores the previous binary on timeout.
|
- `run.self-update`: downloads an approved same-server target-matched distribution in bounded resumable ranges, verifies the final checksum, safely extracts exactly the expected executable, preserves config, and reports a rollback-safe staged result. A helper activates only after result acceptance, then waits for health and restores the previous binary on timeout.
|
||||||
- `dependencies.check`: runs a plugin-declared typed dependency probe addressed by a logical `dependencies/...` key.
|
- `dependencies.check`: runs a plugin-declared typed dependency probe addressed by a logical `dependencies/...` key.
|
||||||
- `dependencies.install`: runs only an approved typed install plan addressed by `dependencies/install/...`; package, verified HTTPS download, SteamCMD, and manual steps are closed adapters, and arbitrary shell snippets, unsafe URLs/tokens, and unsupported targets are rejected.
|
- `dependencies.install`: runs only an approved typed install plan addressed by `dependencies/install/...`; package, verified HTTPS download, and manual steps are closed adapters, and arbitrary shell snippets, unsafe URLs/tokens, and unsupported targets are rejected.
|
||||||
- `logs.backfill`: advances historical log cursors for declared process, file, FTP, SQL, or plugin-specific sources and returns bounded cursor/result refs instead of log bodies.
|
- `logs.backfill`: advances historical log cursors for declared process, file, FTP, SQL, or plugin-specific sources and returns bounded cursor/result refs instead of log bodies.
|
||||||
|
|
||||||
The executor resolves lifecycle action templates under the scoped server workspace and runs direct command/argument vectors through the process supervisor. It does not run unrestricted shell strings, execute arbitrary plugin code, expose host paths, return raw credentials, open direct sockets, or embed logs/artifacts in job result payloads.
|
The executor resolves lifecycle action templates under the scoped server workspace and runs direct command/argument vectors through the process supervisor. It does not run unrestricted shell strings, execute arbitrary plugin code, expose host paths, return raw credentials, open direct sockets, or embed logs/artifacts in job result payloads.
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ func validateRuntimeDLLExtensionPlan(plan RuntimeDLLExtensionPlan) error {
|
|||||||
if !ValidLogicalFileKey(plan.Key) || !ValidLogicalFileKey(plan.TargetKey) || !validExtensionVersion(plan.Version) {
|
if !ValidLogicalFileKey(plan.Key) || !ValidLogicalFileKey(plan.TargetKey) || !validExtensionVersion(plan.Version) {
|
||||||
return ValidationError("DLL extension identity is not allowed")
|
return ValidationError("DLL extension identity is not allowed")
|
||||||
}
|
}
|
||||||
if !validRuntimeDLLURL(plan.ReleaseURL) || !validSHA256(plan.Checksum) || !validSHA256(plan.SCUMExecutableChecksum) || plan.SizeBytes < 1 || plan.SizeBytes > maxRunDLLExtensionBytes {
|
if !validRuntimeDLLURL(plan.ReleaseURL) || !validSHA256(plan.Checksum) || !validSHA256(plan.TargetExecutableChecksum) || plan.SizeBytes < 1 || plan.SizeBytes > maxRunDLLExtensionBytes {
|
||||||
return ValidationError("DLL extension release integrity is not allowed")
|
return ValidationError("DLL extension release integrity is not allowed")
|
||||||
}
|
}
|
||||||
if !validDLLModKey(plan.ModKey) || plan.DLLRef != "ue4ss/Mods/"+plan.ModKey+"/dlls/main.dll" {
|
if !validDLLModKey(plan.ModKey) || plan.DLLRef != "ue4ss/Mods/"+plan.ModKey+"/dlls/main.dll" {
|
||||||
|
|||||||
@@ -64,14 +64,14 @@ func TestValidateRunJobAssignmentRequiresBoundedSQLiteSchemaProbe(t *testing.T)
|
|||||||
|
|
||||||
func TestValidateRunJobAssignmentRejectsLegacyGameSpecificDeploymentPlan(t *testing.T) {
|
func TestValidateRunJobAssignmentRejectsLegacyGameSpecificDeploymentPlan(t *testing.T) {
|
||||||
assignment := RunJobAssignment{
|
assignment := RunJobAssignment{
|
||||||
JobID: "job-legacy-scum-plan",
|
JobID: "job-legacy-game-plan",
|
||||||
ServerInstanceID: "server-1",
|
ServerInstanceID: "server-1",
|
||||||
RunEndpointID: "run-local",
|
RunEndpointID: "run-local",
|
||||||
Capability: RunCapabilityProcessInstall,
|
Capability: RunCapabilityProcessInstall,
|
||||||
IdempotencyKey: "idem-legacy-scum",
|
IdempotencyKey: "idem-legacy-game-plan",
|
||||||
ExecutionInput: RunJobExecutionInput{
|
ExecutionInput: RunJobExecutionInput{
|
||||||
Deployment: &ServerDeploymentExecution{SchemaVersion: "1", Mode: "guided-install", ServerRoot: "C:/scumserver", Revision: 1},
|
Deployment: &ServerDeploymentExecution{SchemaVersion: "1", Mode: "guided-install", ServerRoot: "C:/game-server", Revision: 1},
|
||||||
ServerDeploymentPlan: &ServerDeploymentPlan{SchemaVersion: "1", PluginID: "game.scum", TemplateKey: "scum-steamcmd-windows"},
|
ServerDeploymentPlan: &ServerDeploymentPlan{SchemaVersion: "1", PluginID: "game.example", TemplateKey: "legacy-game-template"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,7 +369,7 @@ func validRuntimeDLLExtensionPlan() RuntimeDLLExtensionPlan {
|
|||||||
TargetKey: "ue4ss/scum-simple-rcon",
|
TargetKey: "ue4ss/scum-simple-rcon",
|
||||||
ModKey: "scum_simple_rcon",
|
ModKey: "scum_simple_rcon",
|
||||||
DLLRef: "ue4ss/Mods/scum_simple_rcon/dlls/main.dll",
|
DLLRef: "ue4ss/Mods/scum_simple_rcon/dlls/main.dll",
|
||||||
SCUMExecutableChecksum: "sha256:" + strings.Repeat("b", 64),
|
TargetExecutableChecksum: "sha256:" + strings.Repeat("b", 64),
|
||||||
UE4SSABI: "ue4ss-3.0",
|
UE4SSABI: "ue4ss-3.0",
|
||||||
RCONPort: 27015,
|
RCONPort: 27015,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,8 +170,8 @@ func TestWorkerDefersUnmatchedRequiredDependencyToBootstrap(t *testing.T) {
|
|||||||
TargetArch: runtime.GOARCH,
|
TargetArch: runtime.GOARCH,
|
||||||
TargetRelease: "run-dist-test",
|
TargetRelease: "run-dist-test",
|
||||||
Bootstrap: &protocol.RunAutonomousLifecycleAction{Action: "start", Operation: "start", Capability: protocol.RunCapabilityProcessStart, TargetKey: "actions/start.json"},
|
Bootstrap: &protocol.RunAutonomousLifecycleAction{Action: "start", Operation: "start", Capability: protocol.RunCapabilityProcessStart, TargetKey: "actions/start.json"},
|
||||||
DependencyProbes: []protocol.DependencyProbe{{Key: "steamcmd", Kind: "command.version", TargetKey: "steamcmd", Required: true, Platforms: []string{runtime.GOOS}}},
|
DependencyProbes: []protocol.DependencyProbe{{Key: "game-runtime", Kind: "command.version", TargetKey: "game-runtime", Required: true, Platforms: []string{runtime.GOOS}}},
|
||||||
InstallPlans: []protocol.DependencyInstallPlan{{Key: "install-game-server", Title: "Install game server", Platforms: []string{runtime.GOOS}, Steps: []protocol.DependencyInstallStep{{Type: "steamcmd-app", TargetKey: "server/install-root", PackageManager: "steamcmd", PackageName: "3792580"}}}},
|
InstallPlans: []protocol.DependencyInstallPlan{{Key: "install-game-server", Title: "Install game server", Platforms: []string{runtime.GOOS}, Steps: []protocol.DependencyInstallStep{{Type: "package", TargetKey: "game-server", PackageManager: "apt", PackageName: "game-server-runtime"}}}},
|
||||||
})
|
})
|
||||||
managed := &recordingManagedSupervisor{}
|
managed := &recordingManagedSupervisor{}
|
||||||
worker, err := NewWorker(cfg, client, WithManagedProcessSupervisor(managed), WithDependencyCommandRunner(missingCommandRunner{}))
|
worker, err := NewWorker(cfg, client, WithManagedProcessSupervisor(managed), WithDependencyCommandRunner(missingCommandRunner{}))
|
||||||
|
|||||||
+14
-20
@@ -31,7 +31,6 @@ const (
|
|||||||
var (
|
var (
|
||||||
dependencyTokenPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_.:+@/-]{0,119}$`)
|
dependencyTokenPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_.:+@/-]{0,119}$`)
|
||||||
dependencyVersionPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_.:+~-]{0,79}$`)
|
dependencyVersionPattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_.:+~-]{0,79}$`)
|
||||||
steamAppPattern = regexp.MustCompile(`^[0-9]{1,12}$`)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type DependencyDownloader interface {
|
type DependencyDownloader interface {
|
||||||
@@ -216,9 +215,23 @@ func validateDependencyInput(assignment protocol.RunJobAssignment, input protoco
|
|||||||
if assignment.Capability != protocol.RunCapabilityDependenciesInstall || assignment.TargetKey != "dependencies/install/"+input.Plan.Key || !protocol.ValidLogicalFileKey(input.Plan.Key) || len(input.Plan.Steps) == 0 || len(input.Plan.Steps) > 64 {
|
if assignment.Capability != protocol.RunCapabilityDependenciesInstall || assignment.TargetKey != "dependencies/install/"+input.Plan.Key || !protocol.ValidLogicalFileKey(input.Plan.Key) || len(input.Plan.Steps) == 0 || len(input.Plan.Steps) > 64 {
|
||||||
return fmt.Errorf("dependency install declaration does not match job")
|
return fmt.Errorf("dependency install declaration does not match job")
|
||||||
}
|
}
|
||||||
|
for _, step := range input.Plan.Steps {
|
||||||
|
if !validDependencyInstallStepType(step.Type) {
|
||||||
|
return fmt.Errorf("dependency install step type is unsupported")
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validDependencyInstallStepType(value string) bool {
|
||||||
|
switch value {
|
||||||
|
case "package", "verified-download", "manual":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (executor LifecycleExecutor) runDependencyProbe(ctx context.Context, probe protocol.DependencyProbe, bindings map[string]string) (string, string, error) {
|
func (executor LifecycleExecutor) runDependencyProbe(ctx context.Context, probe protocol.DependencyProbe, bindings map[string]string) (string, string, error) {
|
||||||
target := strings.TrimSpace(bindings[probe.TargetKey])
|
target := strings.TrimSpace(bindings[probe.TargetKey])
|
||||||
if target == "" {
|
if target == "" {
|
||||||
@@ -308,25 +321,6 @@ func (executor LifecycleExecutor) runDependencyInstallStep(ctx context.Context,
|
|||||||
return fmt.Errorf("typed package adapter failed")
|
return fmt.Errorf("typed package adapter failed")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
case "steamcmd-app":
|
|
||||||
if !steamAppPattern.MatchString(step.PackageName) {
|
|
||||||
return fmt.Errorf("Steam app identifier is unsafe")
|
|
||||||
}
|
|
||||||
executable := strings.TrimSpace(input.Bindings[step.TargetKey])
|
|
||||||
if executable == "" {
|
|
||||||
executable = "steamcmd"
|
|
||||||
}
|
|
||||||
if err := validateDependencyExecutable(executable); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
result, runErr := executor.dependencyRunner.Run(ctx, ProcessCommand{Args: []string{executable, "+login", "anonymous", "+app_update", step.PackageName, "validate", "+quit"}, Timeout: dependencyCommandTimeout, JobID: assignment.JobID, Capability: assignment.Capability, Action: "dependency.steamcmd-app"})
|
|
||||||
if runErr != nil || result.ExitCode != 0 {
|
|
||||||
if ctx.Err() != nil {
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
return fmt.Errorf("typed SteamCMD adapter failed")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
case "verified-download":
|
case "verified-download":
|
||||||
if !validSHA256(step.Checksum) {
|
if !validSHA256(step.Checksum) {
|
||||||
return fmt.Errorf("verified download checksum is required")
|
return fmt.Errorf("verified download checksum is required")
|
||||||
|
|||||||
@@ -155,6 +155,16 @@ func TestVerifiedDependencyDownloadUsesHTTPSChecksumAndScopedDestination(t *test
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDependencyInstallRejectsLegacySteamCMDAppAdapter(t *testing.T) {
|
||||||
|
executor := NewLifecycleExecutor(WithLifecycleWorkspaceRoot(t.TempDir()), WithDependencyCommandRunner(&dependencyTestSupervisor{}))
|
||||||
|
assignment := dependencyAssignment(protocol.RunCapabilityDependenciesInstall)
|
||||||
|
input := dependencyInputForAssignment(assignment)
|
||||||
|
step := protocol.DependencyInstallStep{Type: "steamcmd-app", TargetKey: "server/install-root", PackageManager: "steamcmd", PackageName: "123456"}
|
||||||
|
if err := executor.runDependencyInstallStep(context.Background(), assignment, input, step, 0); err == nil || !strings.Contains(err.Error(), "unsupported") {
|
||||||
|
t.Fatalf("expected legacy SteamCMD app adapter rejection, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func dependencyAssignment(capability string) protocol.RunJobAssignment {
|
func dependencyAssignment(capability string) protocol.RunJobAssignment {
|
||||||
assignment := workerJobAssignment(capability)
|
assignment := workerJobAssignment(capability)
|
||||||
assignment.LeaseToken = "lease-dependency"
|
assignment.LeaseToken = "lease-dependency"
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func TestWorkerDistributionBuildCompilesAndUploadsRawRunExecutable(t *testing.T)
|
|||||||
TargetOS: runtime.GOOS,
|
TargetOS: runtime.GOOS,
|
||||||
TargetArch: runtime.GOARCH,
|
TargetArch: runtime.GOARCH,
|
||||||
TargetRelease: "run-release-test",
|
TargetRelease: "run-release-test",
|
||||||
PlatformURL: "https://scum.npc0.com",
|
PlatformURL: "https://platform.example.test",
|
||||||
PackageFormat: "raw-executable",
|
PackageFormat: "raw-executable",
|
||||||
ArtifactID: "artifact-built-run",
|
ArtifactID: "artifact-built-run",
|
||||||
OutputFilename: output,
|
OutputFilename: output,
|
||||||
@@ -85,7 +85,7 @@ func TestWorkerDistributionBuildCompilesAndUploadsRawRunExecutable(t *testing.T)
|
|||||||
if summary["status"] != "ok" || summary["mode"] != "smoke" {
|
if summary["status"] != "ok" || summary["mode"] != "smoke" {
|
||||||
t.Fatalf("expected generated package to run smoke mode, got %+v", summary)
|
t.Fatalf("expected generated package to run smoke mode, got %+v", summary)
|
||||||
}
|
}
|
||||||
if summary["platformUrl"] != "https://scum.npc0.com" {
|
if summary["platformUrl"] != "https://platform.example.test" {
|
||||||
t.Fatalf("expected generated executable to use compiled platform URL, got %+v", summary)
|
t.Fatalf("expected generated executable to use compiled platform URL, got %+v", summary)
|
||||||
}
|
}
|
||||||
joinedProgress := make([]string, 0, len(client.progressRequests))
|
joinedProgress := make([]string, 0, len(client.progressRequests))
|
||||||
@@ -111,7 +111,7 @@ func TestWorkerDistributionBuildCrossCompilesWindowsAMD64Run(t *testing.T) {
|
|||||||
worker.state.SessionToken = "session-token"
|
worker.state.SessionToken = "session-token"
|
||||||
assignment := protocol.RunJobAssignment{JobID: "job-distribution-build-windows", ServerInstanceID: "server-build-windows", RunEndpointID: "run-builder", Capability: protocol.RunCapabilityDistributionBuild, TargetKey: "distribution/run", InputRef: "input://distribution-build/windows", IdempotencyKey: "distribution-build:windows", State: "running", LeaseToken: "lease-windows", Attempt: 1}
|
assignment := protocol.RunJobAssignment{JobID: "job-distribution-build-windows", ServerInstanceID: "server-build-windows", RunEndpointID: "run-builder", Capability: protocol.RunCapabilityDistributionBuild, TargetKey: "distribution/run", InputRef: "input://distribution-build/windows", IdempotencyKey: "distribution-build:windows", State: "running", LeaseToken: "lease-windows", Attempt: 1}
|
||||||
client.claimJob = assignment
|
client.claimJob = assignment
|
||||||
client.buildInput = protocol.DistributionBuildInputResponse{JobID: assignment.JobID, ComponentKind: "run", ServerInstanceID: assignment.ServerInstanceID, PluginID: "game.scum", RunEndpointID: "server-run-server-build-windows", TargetOS: "windows", TargetArch: "amd64", TargetRelease: "run-release-windows", PlatformURL: "https://scum.npc0.com", PackageFormat: "raw-executable", ArtifactID: "artifact-built-windows-run", OutputFilename: "run.exe", SecretRef: "secret://runtime-keys/server-build-windows/run/current", KeyGeneration: 1, AuthKey: "test-component-key"}
|
client.buildInput = protocol.DistributionBuildInputResponse{JobID: assignment.JobID, ComponentKind: "run", ServerInstanceID: assignment.ServerInstanceID, PluginID: "game.scum", RunEndpointID: "server-run-server-build-windows", TargetOS: "windows", TargetArch: "amd64", TargetRelease: "run-release-windows", PlatformURL: "https://platform.example.test", PackageFormat: "raw-executable", ArtifactID: "artifact-built-windows-run", OutputFilename: "run.exe", SecretRef: "secret://runtime-keys/server-build-windows/run/current", KeyGeneration: 1, AuthKey: "test-component-key"}
|
||||||
|
|
||||||
result := worker.executeDistributionBuild(context.Background(), assignment)
|
result := worker.executeDistributionBuild(context.Background(), assignment)
|
||||||
if result.State != lifecycleResultStateSucceeded || result.ResultRef != "artifact://artifact-built-windows-run" {
|
if result.State != lifecycleResultStateSucceeded || result.ResultRef != "artifact://artifact-built-windows-run" {
|
||||||
@@ -135,7 +135,7 @@ func TestDistributionBuildIsolationUsesPluginJobWorkspaceAndDistinctPackageState
|
|||||||
ProfileKey: "run-local",
|
ProfileKey: "run-local",
|
||||||
TargetOS: "linux",
|
TargetOS: "linux",
|
||||||
TargetArch: "amd64",
|
TargetArch: "amd64",
|
||||||
PlatformURL: "https://scum.npc0.com",
|
PlatformURL: "https://platform.example.test",
|
||||||
PackageFormat: "raw-executable",
|
PackageFormat: "raw-executable",
|
||||||
ArtifactID: "artifact-run-dist-scum-alpha",
|
ArtifactID: "artifact-run-dist-scum-alpha",
|
||||||
OutputFilename: "run",
|
OutputFilename: "run",
|
||||||
@@ -158,8 +158,8 @@ func TestDistributionBuildIsolationUsesPluginJobWorkspaceAndDistinctPackageState
|
|||||||
if filepath.Dir(firstWorkspace) != filepath.Dir(secondWorkspace) || filepath.Base(filepath.Dir(firstWorkspace)) != "game-scum" {
|
if filepath.Dir(firstWorkspace) != filepath.Dir(secondWorkspace) || filepath.Base(filepath.Dir(firstWorkspace)) != "game-scum" {
|
||||||
t.Fatalf("expected workspaces under the same plugin queue directory, first=%s second=%s", firstWorkspace, secondWorkspace)
|
t.Fatalf("expected workspaces under the same plugin queue directory, first=%s second=%s", firstWorkspace, secondWorkspace)
|
||||||
}
|
}
|
||||||
firstFlags := buildRunLDFlags(firstInput, "https://scum.npc0.com")
|
firstFlags := buildRunLDFlags(firstInput, "https://platform.example.test")
|
||||||
secondFlags := buildRunLDFlags(secondInput, "https://scum.npc0.com")
|
secondFlags := buildRunLDFlags(secondInput, "https://platform.example.test")
|
||||||
if !strings.Contains(firstFlags, "BuildServerInstanceID=scum-alpha") || !strings.Contains(firstFlags, "BuildRegistrationToken=alpha-component-key") {
|
if !strings.Contains(firstFlags, "BuildServerInstanceID=scum-alpha") || !strings.Contains(firstFlags, "BuildRegistrationToken=alpha-component-key") {
|
||||||
t.Fatalf("expected first build flags to carry first server identity, got %q", firstFlags)
|
t.Fatalf("expected first build flags to carry first server identity, got %q", firstFlags)
|
||||||
}
|
}
|
||||||
@@ -255,7 +255,7 @@ func TestValidateDistributionBuildInputRejectsLegacyClientManagerBuilds(t *testi
|
|||||||
|
|
||||||
func TestValidateDistributionBuildInputAllowsDedicatedRunIdentity(t *testing.T) {
|
func TestValidateDistributionBuildInputAllowsDedicatedRunIdentity(t *testing.T) {
|
||||||
assignment := protocol.RunJobAssignment{JobID: "job-build", ServerInstanceID: "server-build", RunEndpointID: "run-local-debug"}
|
assignment := protocol.RunJobAssignment{JobID: "job-build", ServerInstanceID: "server-build", RunEndpointID: "run-local-debug"}
|
||||||
input := protocol.DistributionBuildInputResponse{JobID: assignment.JobID, ComponentKind: "run", ServerInstanceID: assignment.ServerInstanceID, RunEndpointID: "server-run-server-build", PluginID: "game.scum", TargetOS: "windows", TargetArch: "amd64", TargetRelease: "release-1", PlatformURL: "https://scum.npc0.com", PackageFormat: "raw-executable", ArtifactID: "artifact-build", OutputFilename: "run.exe", AuthKey: "key"}
|
input := protocol.DistributionBuildInputResponse{JobID: assignment.JobID, ComponentKind: "run", ServerInstanceID: assignment.ServerInstanceID, RunEndpointID: "server-run-server-build", PluginID: "game.scum", TargetOS: "windows", TargetArch: "amd64", TargetRelease: "release-1", PlatformURL: "https://platform.example.test", PackageFormat: "raw-executable", ArtifactID: "artifact-build", OutputFilename: "run.exe", AuthKey: "key"}
|
||||||
if err := validateDistributionBuildInput(assignment, input); err != nil {
|
if err := validateDistributionBuildInput(assignment, input); err != nil {
|
||||||
t.Fatalf("dedicated Run identity must be accepted for a builder job: %v", err)
|
t.Fatalf("dedicated Run identity must be accepted for a builder job: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -639,7 +639,7 @@ func TestDeploymentFileExecutorUsesServerRoot(t *testing.T) {
|
|||||||
if err := os.WriteFile(filepath.Join(workspaceRoot, ".platform"), []byte("workspace"), 0o600); err != nil {
|
if err := os.WriteFile(filepath.Join(workspaceRoot, ".platform"), []byte("workspace"), 0o600); err != nil {
|
||||||
t.Fatalf("write workspace marker: %v", err)
|
t.Fatalf("write workspace marker: %v", err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(filepath.Join(serverRoot, "SCUMServer.exe"), []byte("server"), 0o600); err != nil {
|
if err := os.WriteFile(filepath.Join(serverRoot, "GameServer.exe"), []byte("server"), 0o600); err != nil {
|
||||||
t.Fatalf("write server marker: %v", err)
|
t.Fatalf("write server marker: %v", err)
|
||||||
}
|
}
|
||||||
executor, err := NewFileExecutor(workspaceRoot)
|
executor, err := NewFileExecutor(workspaceRoot)
|
||||||
@@ -658,7 +658,7 @@ func TestDeploymentFileExecutorUsesServerRoot(t *testing.T) {
|
|||||||
if err := json.Unmarshal([]byte(result.ExecutionResult.Content), &envelope); err != nil {
|
if err := json.Unmarshal([]byte(result.ExecutionResult.Content), &envelope); err != nil {
|
||||||
t.Fatalf("decode deployment listing: %v", err)
|
t.Fatalf("decode deployment listing: %v", err)
|
||||||
}
|
}
|
||||||
if len(envelope.Entries) != 1 || envelope.Entries[0].Name != "SCUMServer.exe" {
|
if len(envelope.Entries) != 1 || envelope.Entries[0].Name != "GameServer.exe" {
|
||||||
t.Fatalf("expected only deployment root entry, got %+v", envelope.Entries)
|
t.Fatalf("expected only deployment root entry, got %+v", envelope.Entries)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,8 +60,8 @@ func TestGeneratedRunOmitsBuildOnlyCapabilities(t *testing.T) {
|
|||||||
|
|
||||||
func TestLifecycleExecutorRejectsLegacyGameSpecificDeploymentPlan(t *testing.T) {
|
func TestLifecycleExecutorRejectsLegacyGameSpecificDeploymentPlan(t *testing.T) {
|
||||||
assignment := lifecycleAssignment(protocol.RunCapabilityProcessInstall)
|
assignment := lifecycleAssignment(protocol.RunCapabilityProcessInstall)
|
||||||
assignment.ExecutionInput.Deployment = &protocol.ServerDeploymentExecution{SchemaVersion: "1", Mode: "guided-install", ServerRoot: "C:/scumserver", Revision: 1}
|
assignment.ExecutionInput.Deployment = &protocol.ServerDeploymentExecution{SchemaVersion: "1", Mode: "guided-install", ServerRoot: "C:/game-server", Revision: 1}
|
||||||
assignment.ExecutionInput.ServerDeploymentPlan = &protocol.ServerDeploymentPlan{SchemaVersion: "1", PluginID: "game.scum", TemplateKey: "scum-steamcmd-windows"}
|
assignment.ExecutionInput.ServerDeploymentPlan = &protocol.ServerDeploymentPlan{SchemaVersion: "1", PluginID: "game.example", TemplateKey: "legacy-game-template"}
|
||||||
|
|
||||||
result := NewLifecycleExecutor().Execute(assignment)
|
result := NewLifecycleExecutor().Execute(assignment)
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const (
|
|||||||
ue4ssExtensionMarkerVersion = 2
|
ue4ssExtensionMarkerVersion = 2
|
||||||
maxUE4SSMetadataBytes int64 = 16 * 1024
|
maxUE4SSMetadataBytes int64 = 16 * 1024
|
||||||
maxUE4SSDLLBytes int64 = 128 * 1024 * 1024
|
maxUE4SSDLLBytes int64 = 128 * 1024 * 1024
|
||||||
maxSCUMExecutableBytes int64 = 2 * 1024 * 1024 * 1024
|
maxTargetExecutableBytes int64 = 2 * 1024 * 1024 * 1024
|
||||||
managedRCONConfigMarker = "; managed by Run UE4SS DLL extension"
|
managedRCONConfigMarker = "; managed by Run UE4SS DLL extension"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ func (executor LifecycleExecutor) synchronizeUE4SSDLLExtensions(ctx context.Cont
|
|||||||
}
|
}
|
||||||
executableKey := template.TargetExecutableKey
|
executableKey := template.TargetExecutableKey
|
||||||
// Older generated plugin packages do not have targetExecutableKey yet,
|
// Older generated plugin packages do not have targetExecutableKey yet,
|
||||||
// but SCUM's existing start action already carries the same logical path
|
// but the existing start action already carries the same logical path
|
||||||
// in SERVER_EXECUTABLE_REF. Keep those packages forward-compatible.
|
// in SERVER_EXECUTABLE_REF. Keep those packages forward-compatible.
|
||||||
if executableKey == "" && template.Environment != nil {
|
if executableKey == "" && template.Environment != nil {
|
||||||
executableKey = template.Environment["SERVER_EXECUTABLE_REF"]
|
executableKey = template.Environment["SERVER_EXECUTABLE_REF"]
|
||||||
@@ -68,7 +68,7 @@ func (executor LifecycleExecutor) synchronizeUE4SSDLLExtensions(ctx context.Cont
|
|||||||
executableKey = template.ExecutableKey
|
executableKey = template.ExecutableKey
|
||||||
}
|
}
|
||||||
if executableKey == "" || !strings.HasSuffix(strings.ToLower(executableKey), ".exe") {
|
if executableKey == "" || !strings.HasSuffix(strings.ToLower(executableKey), ".exe") {
|
||||||
return dllExtensionError{code: "extension_scum_executable_invalid", message: "UE4SS DLL extensions require a declared SCUM executable"}
|
return dllExtensionError{code: "extension_target_executable_invalid", message: "UE4SS DLL extensions require a declared target executable"}
|
||||||
}
|
}
|
||||||
|
|
||||||
resolver := NewWorkspaceResolver(executor.workspaceRoot)
|
resolver := NewWorkspaceResolver(executor.workspaceRoot)
|
||||||
@@ -76,28 +76,28 @@ func (executor LifecycleExecutor) synchronizeUE4SSDLLExtensions(ctx context.Cont
|
|||||||
if deployment := assignment.ExecutionInput.Deployment; deployment != nil && deployment.ServerRoot != "" {
|
if deployment := assignment.ExecutionInput.Deployment; deployment != nil && deployment.ServerRoot != "" {
|
||||||
root := filepath.Clean(deployment.ServerRoot)
|
root := filepath.Clean(deployment.ServerRoot)
|
||||||
if root == "." || !filepath.IsAbs(root) {
|
if root == "." || !filepath.IsAbs(root) {
|
||||||
return dllExtensionError{code: "extension_scum_executable_invalid", message: "declared executable root is unsafe"}
|
return dllExtensionError{code: "extension_target_executable_invalid", message: "declared executable root is unsafe"}
|
||||||
}
|
}
|
||||||
targetResolver = NewWorkspaceResolver(filepath.Dir(root))
|
targetResolver = NewWorkspaceResolver(filepath.Dir(root))
|
||||||
targetScope = root
|
targetScope = root
|
||||||
}
|
}
|
||||||
executable, err := declaredLifecycleExecutable(targetResolver, targetScope, executableKey)
|
executable, err := declaredLifecycleExecutable(targetResolver, targetScope, executableKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return dllExtensionError{code: "extension_scum_executable_invalid", message: "declared SCUM executable is unavailable"}
|
return dllExtensionError{code: "extension_target_executable_invalid", message: "declared target executable is unavailable"}
|
||||||
}
|
}
|
||||||
executableChecksum, _, err := checksumRegularFile(executable, maxSCUMExecutableBytes)
|
executableChecksum, _, err := checksumRegularFile(executable, maxTargetExecutableBytes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return dllExtensionError{code: "extension_scum_executable_invalid", message: "declared SCUM executable cannot be verified"}
|
return dllExtensionError{code: "extension_target_executable_invalid", message: "declared target executable cannot be verified"}
|
||||||
}
|
}
|
||||||
for _, plan := range assignment.ExecutionInput.DLLExtensions {
|
for _, plan := range assignment.ExecutionInput.DLLExtensions {
|
||||||
if !strings.EqualFold(executableChecksum, plan.SCUMExecutableChecksum) {
|
if !strings.EqualFold(executableChecksum, plan.TargetExecutableChecksum) {
|
||||||
return dllExtensionError{code: "extension_scum_checksum_mismatch", message: "declared SCUM executable does not match the extension release"}
|
return dllExtensionError{code: "extension_target_checksum_mismatch", message: "declared target executable does not match the extension release"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
gameRootKey, err := gameRootKeyForExecutable(template.ExecutableKey)
|
gameRootKey, err := gameRootKeyForExecutable(template.ExecutableKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return dllExtensionError{code: "extension_scum_executable_invalid", message: "declared SCUM executable location is unsafe"}
|
return dllExtensionError{code: "extension_target_executable_invalid", message: "declared target executable location is unsafe"}
|
||||||
}
|
}
|
||||||
if err := verifyUE4SSBootstrap(targetResolver, targetScope, gameRootKey); err != nil {
|
if err := verifyUE4SSBootstrap(targetResolver, targetScope, gameRootKey); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -164,8 +164,8 @@ func TestUE4SSDLLExtensionSynchronizesNoOpsUpdatesAndKeepsPriorRelease(t *testin
|
|||||||
t.Fatalf("expected exactly one enabled managed mod entry, mods=%q", mods)
|
t.Fatalf("expected exactly one enabled managed mod entry, mods=%q", mods)
|
||||||
}
|
}
|
||||||
command := supervisor.Command()
|
command := supervisor.Command()
|
||||||
if len(command.Args) != 1 || filepath.Base(command.Args[0]) != "SCUMServer.exe" || strings.HasSuffix(strings.ToLower(command.Args[0]), ".dll") {
|
if len(command.Args) != 1 || filepath.Base(command.Args[0]) != "GameServer.exe" || strings.HasSuffix(strings.ToLower(command.Args[0]), ".dll") {
|
||||||
t.Fatalf("expected normal SCUM executable start without a DLL loader, command=%+v", command)
|
t.Fatalf("expected normal game executable start without a DLL loader, command=%+v", command)
|
||||||
}
|
}
|
||||||
|
|
||||||
second := executor.Execute(fixture.assignment)
|
second := executor.Execute(fixture.assignment)
|
||||||
@@ -368,9 +368,9 @@ func newUE4SSExtensionFixture(t *testing.T, root string, bootstrap bool) ue4ssEx
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("create workspace scope: %v", err)
|
t.Fatalf("create workspace scope: %v", err)
|
||||||
}
|
}
|
||||||
writeUE4SSFixtureFile(t, filepath.Join(scope, "actions", "start.json"), []byte(`{"version":1,"action":"start","mode":"supervised","executableKey":"bin/SCUMServer.exe"}`), 0o600)
|
writeUE4SSFixtureFile(t, filepath.Join(scope, "actions", "start.json"), []byte(`{"version":1,"action":"start","mode":"supervised","executableKey":"bin/GameServer.exe"}`), 0o600)
|
||||||
executable := []byte("SCUM server executable fixture")
|
executable := []byte("game server executable fixture")
|
||||||
writeUE4SSFixtureFile(t, filepath.Join(scope, "bin", "SCUMServer.exe"), executable, 0o700)
|
writeUE4SSFixtureFile(t, filepath.Join(scope, "bin", "GameServer.exe"), executable, 0o700)
|
||||||
if bootstrap {
|
if bootstrap {
|
||||||
writeUE4SSFixtureFile(t, filepath.Join(scope, "bin", "dwmapi.dll"), []byte("UE4SS proxy fixture"), 0o600)
|
writeUE4SSFixtureFile(t, filepath.Join(scope, "bin", "dwmapi.dll"), []byte("UE4SS proxy fixture"), 0o600)
|
||||||
writeUE4SSFixtureFile(t, filepath.Join(scope, "bin", "UE4SS.dll"), []byte("UE4SS loader fixture"), 0o600)
|
writeUE4SSFixtureFile(t, filepath.Join(scope, "bin", "UE4SS.dll"), []byte("UE4SS loader fixture"), 0o600)
|
||||||
@@ -385,7 +385,7 @@ func newUE4SSExtensionFixture(t *testing.T, root string, bootstrap bool) ue4ssEx
|
|||||||
TargetKey: "ue4ss/scum-simple-rcon",
|
TargetKey: "ue4ss/scum-simple-rcon",
|
||||||
ModKey: "scum_simple_rcon",
|
ModKey: "scum_simple_rcon",
|
||||||
DLLRef: "ue4ss/Mods/scum_simple_rcon/dlls/main.dll",
|
DLLRef: "ue4ss/Mods/scum_simple_rcon/dlls/main.dll",
|
||||||
SCUMExecutableChecksum: bytesChecksum(executable),
|
TargetExecutableChecksum: bytesChecksum(executable),
|
||||||
UE4SSABI: "ue4ss-3.0",
|
UE4SSABI: "ue4ss-3.0",
|
||||||
RCONPort: 27015,
|
RCONPort: 27015,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user