Fix declared server config file reads

This commit is contained in:
npc0-hue
2026-09-21 10:05:50 +08:00
parent b481b1b798
commit 20008b4043
16 changed files with 166 additions and 33 deletions
+3 -2
View File
@@ -329,8 +329,8 @@ type PluginFileWorkspace struct {
type PluginLogicalDirectory struct{ Key, Label, Scope string }
type PluginLogicalFile struct {
Key, DirectoryKey, Label, Kind, StreamKey string
Editable bool
Key, DirectoryKey, Label, Kind, StreamKey, TargetKey string
Editable bool
}
type PluginConfigField struct {
Key, FileKey, ConfigKey, Label, Description, Control, DefaultValue, RestartImpact string
@@ -1209,6 +1209,7 @@ type JobRetryPolicy struct {
type JobExecutionInput struct {
WorkspaceScope string
Content string
FileTargetKey string
ExpectedVersion int
ExpectedChecksum string
MaxReadBytes int
+2 -1
View File
@@ -96,6 +96,7 @@ type RunJobResultRequest struct {
type RunJobExecutionInputBody struct {
WorkspaceScope string `json:"workspaceScope,omitempty"`
Content string `json:"content,omitempty"`
FileTargetKey string `json:"fileTargetKey,omitempty"`
ExpectedVersion int `json:"expectedVersion,omitempty"`
ExpectedChecksum string `json:"expectedChecksum,omitempty"`
MaxReadBytes int `json:"maxReadBytes,omitempty"`
@@ -663,7 +664,7 @@ func RunJobAssignmentFromDomain(assignment domain.RunJobAssignment) RunJobAssign
State: assignment.State,
Progress: progressReportFromDomain(assignment.Progress),
ResultRef: assignment.ResultRef,
ExecutionInput: RunJobExecutionInputBody{WorkspaceScope: assignment.ExecutionInput.WorkspaceScope, Content: assignment.ExecutionInput.Content, ExpectedVersion: assignment.ExecutionInput.ExpectedVersion, ExpectedChecksum: assignment.ExecutionInput.ExpectedChecksum, MaxReadBytes: assignment.ExecutionInput.MaxReadBytes, RemoteAdapterKey: assignment.ExecutionInput.RemoteAdapterKey, RemoteAdapterKind: assignment.ExecutionInput.RemoteAdapterKind, TimeoutSeconds: assignment.ExecutionInput.TimeoutSeconds, PluginID: assignment.ExecutionInput.PluginID, LifecycleOperation: assignment.ExecutionInput.LifecycleOperation, TargetVersion: assignment.ExecutionInput.TargetVersion, Inputs: domain.CopyStringMap(assignment.ExecutionInput.Inputs), LogSource: runtimeLogSourceFromDomain(assignment.ExecutionInput.LogSource), LogSources: runtimeLogSourcesFromDomain(assignment.ExecutionInput.LogSources), DLLExtensions: dllExtensionPlansFromDomain(assignment.ExecutionInput.DLLExtensions), SourceRCON: runtimeSourceRCONPlanFromDomain(assignment.ExecutionInput.SourceRCON), Deployment: deploymentExecutionFromDomain(assignment.ExecutionInput.Deployment), ServerDeploymentPlan: serverDeploymentPlanFromDomain(assignment.ExecutionInput.ServerDeploymentPlan)},
ExecutionInput: RunJobExecutionInputBody{WorkspaceScope: assignment.ExecutionInput.WorkspaceScope, Content: assignment.ExecutionInput.Content, FileTargetKey: assignment.ExecutionInput.FileTargetKey, ExpectedVersion: assignment.ExecutionInput.ExpectedVersion, ExpectedChecksum: assignment.ExecutionInput.ExpectedChecksum, MaxReadBytes: assignment.ExecutionInput.MaxReadBytes, RemoteAdapterKey: assignment.ExecutionInput.RemoteAdapterKey, RemoteAdapterKind: assignment.ExecutionInput.RemoteAdapterKind, TimeoutSeconds: assignment.ExecutionInput.TimeoutSeconds, PluginID: assignment.ExecutionInput.PluginID, LifecycleOperation: assignment.ExecutionInput.LifecycleOperation, TargetVersion: assignment.ExecutionInput.TargetVersion, Inputs: domain.CopyStringMap(assignment.ExecutionInput.Inputs), LogSource: runtimeLogSourceFromDomain(assignment.ExecutionInput.LogSource), LogSources: runtimeLogSourcesFromDomain(assignment.ExecutionInput.LogSources), DLLExtensions: dllExtensionPlansFromDomain(assignment.ExecutionInput.DLLExtensions), SourceRCON: runtimeSourceRCONPlanFromDomain(assignment.ExecutionInput.SourceRCON), Deployment: deploymentExecutionFromDomain(assignment.ExecutionInput.Deployment), ServerDeploymentPlan: serverDeploymentPlanFromDomain(assignment.ExecutionInput.ServerDeploymentPlan)},
LeaseToken: assignment.LeaseToken,
Attempt: assignment.Attempt,
FencingToken: assignment.FencingToken,
+3 -2
View File
@@ -198,6 +198,7 @@ type PluginLogicalFileBody struct {
Label string `json:"label"`
Kind string `json:"kind"`
StreamKey string `json:"streamKey,omitempty"`
TargetKey string `json:"targetKey,omitempty"`
Editable bool `json:"editable,omitempty"`
}
type PluginConfigFieldBody struct {
@@ -1146,7 +1147,7 @@ func fileWorkspaceToDomain(body PluginFileWorkspaceBody) domain.PluginFileWorksp
workspace.Directories = append(workspace.Directories, domain.PluginLogicalDirectory{Key: item.Key, Label: item.Label, Scope: item.Scope})
}
for _, item := range body.Files {
workspace.Files = append(workspace.Files, domain.PluginLogicalFile{Key: item.Key, DirectoryKey: item.DirectoryKey, Label: item.Label, Kind: item.Kind, StreamKey: item.StreamKey, Editable: item.Editable})
workspace.Files = append(workspace.Files, domain.PluginLogicalFile{Key: item.Key, DirectoryKey: item.DirectoryKey, Label: item.Label, Kind: item.Kind, StreamKey: item.StreamKey, TargetKey: item.TargetKey, Editable: item.Editable})
}
for _, item := range body.ConfigFields {
workspace.ConfigFields = append(workspace.ConfigFields, domain.PluginConfigField{Key: item.Key, FileKey: item.FileKey, ConfigKey: item.ConfigKey, Label: item.Label, Description: item.Description, Control: item.Control, Minimum: item.Minimum, Maximum: item.Maximum, DefaultValue: item.DefaultValue, RestartImpact: item.RestartImpact})
@@ -1160,7 +1161,7 @@ func fileWorkspaceFromDomain(workspace domain.PluginFileWorkspace) PluginFileWor
body.Directories = append(body.Directories, PluginLogicalDirectoryBody{Key: item.Key, Label: item.Label, Scope: item.Scope})
}
for _, item := range workspace.Files {
body.Files = append(body.Files, PluginLogicalFileBody{Key: item.Key, DirectoryKey: item.DirectoryKey, Label: item.Label, Kind: item.Kind, StreamKey: item.StreamKey, Editable: item.Editable})
body.Files = append(body.Files, PluginLogicalFileBody{Key: item.Key, DirectoryKey: item.DirectoryKey, Label: item.Label, Kind: item.Kind, StreamKey: item.StreamKey, TargetKey: item.TargetKey, Editable: item.Editable})
}
for _, item := range workspace.ConfigFields {
body.ConfigFields = append(body.ConfigFields, PluginConfigFieldBody{Key: item.Key, FileKey: item.FileKey, ConfigKey: item.ConfigKey, Label: item.Label, Description: item.Description, Control: item.Control, Minimum: item.Minimum, Maximum: item.Maximum, DefaultValue: item.DefaultValue, RestartImpact: item.RestartImpact})
+3 -2
View File
@@ -291,6 +291,7 @@ type JobRetryPolicy struct {
type JobExecutionInput struct {
WorkspaceScope string `json:"workspaceScope,omitempty" db:"workspace_scope"`
Content string `json:"content,omitempty" db:"content"`
FileTargetKey string `json:"fileTargetKey,omitempty" db:"file_target_key"`
ExpectedVersion int `json:"expectedVersion,omitempty" db:"expected_version"`
ExpectedChecksum string `json:"expectedChecksum,omitempty" db:"expected_checksum"`
MaxReadBytes int `json:"maxReadBytes,omitempty" db:"max_read_bytes"`
@@ -868,7 +869,7 @@ func executionInputFromDomain(input domain.JobExecutionInput) JobExecutionInput
copy := domain.CopyServerDeploymentDefinition(*input.Deployment)
deployment = &copy
}
return JobExecutionInput{WorkspaceScope: input.WorkspaceScope, Content: input.Content, ExpectedVersion: input.ExpectedVersion, ExpectedChecksum: input.ExpectedChecksum, MaxReadBytes: input.MaxReadBytes, RemoteAdapterKey: input.RemoteAdapterKey, RemoteAdapterKind: input.RemoteAdapterKind, TimeoutSeconds: input.TimeoutSeconds, PluginID: input.PluginID, LifecycleOperation: input.LifecycleOperation, TargetVersion: input.TargetVersion, Inputs: domain.CopyStringMap(input.Inputs), DLLExtensions: append([]domain.RuntimeDLLExtensionPlan(nil), input.DLLExtensions...), SourceRCON: domain.CopyRuntimeSourceRCONPlan(input.SourceRCON), Deployment: deployment, ServerDeploymentPlan: domain.CopyServerDeploymentPlan(input.ServerDeploymentPlan)}
return JobExecutionInput{WorkspaceScope: input.WorkspaceScope, Content: input.Content, FileTargetKey: input.FileTargetKey, ExpectedVersion: input.ExpectedVersion, ExpectedChecksum: input.ExpectedChecksum, MaxReadBytes: input.MaxReadBytes, RemoteAdapterKey: input.RemoteAdapterKey, RemoteAdapterKind: input.RemoteAdapterKind, TimeoutSeconds: input.TimeoutSeconds, PluginID: input.PluginID, LifecycleOperation: input.LifecycleOperation, TargetVersion: input.TargetVersion, Inputs: domain.CopyStringMap(input.Inputs), DLLExtensions: append([]domain.RuntimeDLLExtensionPlan(nil), input.DLLExtensions...), SourceRCON: domain.CopyRuntimeSourceRCONPlan(input.SourceRCON), Deployment: deployment, ServerDeploymentPlan: domain.CopyServerDeploymentPlan(input.ServerDeploymentPlan)}
}
func (input JobExecutionInput) ToDomain() domain.JobExecutionInput {
@@ -877,7 +878,7 @@ func (input JobExecutionInput) ToDomain() domain.JobExecutionInput {
copy := domain.CopyServerDeploymentDefinition(*input.Deployment)
deployment = &copy
}
return domain.JobExecutionInput{WorkspaceScope: input.WorkspaceScope, Content: input.Content, ExpectedVersion: input.ExpectedVersion, ExpectedChecksum: input.ExpectedChecksum, MaxReadBytes: input.MaxReadBytes, RemoteAdapterKey: input.RemoteAdapterKey, RemoteAdapterKind: input.RemoteAdapterKind, TimeoutSeconds: input.TimeoutSeconds, PluginID: input.PluginID, LifecycleOperation: input.LifecycleOperation, TargetVersion: input.TargetVersion, Inputs: domain.CopyStringMap(input.Inputs), DLLExtensions: append([]domain.RuntimeDLLExtensionPlan(nil), input.DLLExtensions...), SourceRCON: domain.CopyRuntimeSourceRCONPlan(input.SourceRCON), Deployment: deployment, ServerDeploymentPlan: domain.CopyServerDeploymentPlan(input.ServerDeploymentPlan)}
return domain.JobExecutionInput{WorkspaceScope: input.WorkspaceScope, Content: input.Content, FileTargetKey: input.FileTargetKey, ExpectedVersion: input.ExpectedVersion, ExpectedChecksum: input.ExpectedChecksum, MaxReadBytes: input.MaxReadBytes, RemoteAdapterKey: input.RemoteAdapterKey, RemoteAdapterKind: input.RemoteAdapterKind, TimeoutSeconds: input.TimeoutSeconds, PluginID: input.PluginID, LifecycleOperation: input.LifecycleOperation, TargetVersion: input.TargetVersion, Inputs: domain.CopyStringMap(input.Inputs), DLLExtensions: append([]domain.RuntimeDLLExtensionPlan(nil), input.DLLExtensions...), SourceRCON: domain.CopyRuntimeSourceRCONPlan(input.SourceRCON), Deployment: deployment, ServerDeploymentPlan: domain.CopyServerDeploymentPlan(input.ServerDeploymentPlan)}
}
func executionResultFromDomain(result domain.JobExecutionResult) JobExecutionResult {
+1
View File
@@ -95,6 +95,7 @@ func TestGamePluginModelRoundTripCopiesSlices(t *testing.T) {
func TestJobExecutionInputModelRoundTripPreservesLifecycleMetadata(t *testing.T) {
source := domain.JobExecutionInput{
WorkspaceScope: "server-workspace",
FileTargetKey: "SCUM/Saved/Config/WindowsServer/ServerSettings.ini",
PluginID: "game.scum",
LifecycleOperation: "upgrade",
TargetVersion: "2.0.0",
+1 -1
View File
@@ -691,7 +691,7 @@ func assignmentFromJob(job domain.Job, leaseToken string) domain.RunJobAssignmen
State: job.State,
Progress: domain.RunJobProgressReport{Percent: job.Progress.Percent, Phase: job.Progress.Phase, Message: job.Progress.Message},
ResultRef: job.ResultRef,
ExecutionInput: domain.JobExecutionInput{WorkspaceScope: job.ExecutionInput.WorkspaceScope, Content: job.ExecutionInput.Content, ExpectedVersion: job.ExecutionInput.ExpectedVersion, ExpectedChecksum: job.ExecutionInput.ExpectedChecksum, MaxReadBytes: job.ExecutionInput.MaxReadBytes, RemoteAdapterKey: job.ExecutionInput.RemoteAdapterKey, RemoteAdapterKind: job.ExecutionInput.RemoteAdapterKind, TimeoutSeconds: job.ExecutionInput.TimeoutSeconds, PluginID: job.ExecutionInput.PluginID, LifecycleOperation: job.ExecutionInput.LifecycleOperation, TargetVersion: job.ExecutionInput.TargetVersion, Inputs: domain.CopyStringMap(job.ExecutionInput.Inputs), LogSource: domain.CopyRuntimeLogSourcePtr(job.ExecutionInput.LogSource), LogSources: domain.CopyRuntimeLogSources(job.ExecutionInput.LogSources), DLLExtensions: append([]domain.RuntimeDLLExtensionPlan(nil), job.ExecutionInput.DLLExtensions...), SourceRCON: domain.CopyRuntimeSourceRCONPlan(job.ExecutionInput.SourceRCON), Deployment: deploymentPlanForDispatchValue(job.ExecutionInput.Deployment), ServerDeploymentPlan: domain.CopyServerDeploymentPlan(job.ExecutionInput.ServerDeploymentPlan)},
ExecutionInput: domain.JobExecutionInput{WorkspaceScope: job.ExecutionInput.WorkspaceScope, Content: job.ExecutionInput.Content, FileTargetKey: job.ExecutionInput.FileTargetKey, ExpectedVersion: job.ExecutionInput.ExpectedVersion, ExpectedChecksum: job.ExecutionInput.ExpectedChecksum, MaxReadBytes: job.ExecutionInput.MaxReadBytes, RemoteAdapterKey: job.ExecutionInput.RemoteAdapterKey, RemoteAdapterKind: job.ExecutionInput.RemoteAdapterKind, TimeoutSeconds: job.ExecutionInput.TimeoutSeconds, PluginID: job.ExecutionInput.PluginID, LifecycleOperation: job.ExecutionInput.LifecycleOperation, TargetVersion: job.ExecutionInput.TargetVersion, Inputs: domain.CopyStringMap(job.ExecutionInput.Inputs), LogSource: domain.CopyRuntimeLogSourcePtr(job.ExecutionInput.LogSource), LogSources: domain.CopyRuntimeLogSources(job.ExecutionInput.LogSources), DLLExtensions: append([]domain.RuntimeDLLExtensionPlan(nil), job.ExecutionInput.DLLExtensions...), SourceRCON: domain.CopyRuntimeSourceRCONPlan(job.ExecutionInput.SourceRCON), Deployment: deploymentPlanForDispatchValue(job.ExecutionInput.Deployment), ServerDeploymentPlan: domain.CopyServerDeploymentPlan(job.ExecutionInput.ServerDeploymentPlan)},
LeaseToken: leaseToken,
Attempt: job.Attempt,
FencingToken: 0,
+54 -9
View File
@@ -2114,6 +2114,7 @@ func (svc *CoreService) GetDeclaredFileReadSnapshotForSession(sessionID string,
}
var completed *domain.Job
var pending *domain.Job
var terminalFailure *domain.Job
for i := range jobs {
job := jobs[i]
if job.Capability != domain.JobCapabilityFilesRead || job.TargetKey != fileKey {
@@ -2129,6 +2130,11 @@ func (svc *CoreService) GetDeclaredFileReadSnapshotForSession(sessionID string,
if declaredFileReadPendingState(job.State) && (pending == nil || newerJob(job, *pending)) {
copy := job
pending = &copy
continue
}
if (job.State == domain.JobStateFailed || job.State == domain.JobStateCancelled) && (terminalFailure == nil || newerJob(job, *terminalFailure)) {
copy := job
terminalFailure = &copy
}
}
base := domain.DeclaredFileReadSnapshot{ServerInstanceID: instance.ID, PluginID: plugin.ID, Key: fileKey}
@@ -2152,6 +2158,16 @@ func (svc *CoreService) GetDeclaredFileReadSnapshotForSession(sessionID string,
base.Reason = "等待运行端完成文件读取。"
return base, nil
}
if terminalFailure != nil {
base.State = "failed"
if terminalFailure.State == domain.JobStateCancelled {
base.State = "cancelled"
}
base.JobID = terminalFailure.ID
base.ReadAt = jobCompletedAt(*terminalFailure)
base.Reason = declaredFileReadFailureReason(*terminalFailure)
return base, nil
}
base.State = "not-read"
base.Reason = "尚未读取此文件。"
return base, nil
@@ -2166,6 +2182,24 @@ func declaredFileReadPendingState(state domain.JobState) bool {
}
}
func declaredFileReadFailureReason(job domain.Job) string {
prefix := "Run 文件读取失败"
if job.State == domain.JobStateCancelled {
prefix = "Run 文件读取已取消"
}
detail := strings.TrimSpace(job.CancelReason)
if detail == "" {
detail = strings.TrimSpace(job.Progress.Message)
}
if detail == "" {
detail = strings.TrimSpace(job.ExecutionResult.Summary)
}
if detail == "" {
return prefix + "。"
}
return prefix + "" + detail
}
func newerJob(left domain.Job, right domain.Job) bool {
leftTime, rightTime := jobCompletedAt(left), jobCompletedAt(right)
if !leftTime.Equal(rightTime) {
@@ -2293,14 +2327,18 @@ func (svc *CoreService) DispatchFileOperationForSession(sessionID string, reques
return domain.FileOperationDispatchResult{}, ErrForbidden
}
}
pluginID := strings.TrimSpace(request.PluginID)
if pluginID == "" {
pluginID = instance.PluginID
}
plugin, err := svc.store.GamePlugins().Get(pluginID)
if err != nil {
return domain.FileOperationDispatchResult{}, err
}
if plugin.ID != instance.PluginID {
return domain.FileOperationDispatchResult{}, validationError("pluginId must match server instance")
}
if request.PluginID != "" {
plugin, err := svc.store.GamePlugins().Get(request.PluginID)
if err != nil {
return domain.FileOperationDispatchResult{}, err
}
if plugin.ID != instance.PluginID {
return domain.FileOperationDispatchResult{}, validationError("pluginId must match server instance")
}
if plugin.Status != domain.GamePluginStatusInstalled {
return domain.FileOperationDispatchResult{}, validationError("plugin must be installed")
}
@@ -2311,6 +2349,13 @@ func (svc *CoreService) DispatchFileOperationForSession(sessionID string, reques
return domain.FileOperationDispatchResult{}, ErrForbidden
}
}
fileTargetKey := ""
if request.Operation != domain.FileOperationList {
file, declared, allowed := declaredPluginFileRequest(plugin.FileWorkspace, request)
if declared && allowed {
fileTargetKey = file.TargetKey
}
}
capability := domain.JobCapabilityFilesRead
message := "file read queued"
if request.Operation == domain.FileOperationList {
@@ -2328,7 +2373,7 @@ func (svc *CoreService) DispatchFileOperationForSession(sessionID string, reques
Capability: capability,
TargetKey: request.Key,
InputRef: request.InputRef,
ExecutionInput: domain.JobExecutionInput{WorkspaceScope: svc.runtimeProfileScope(instance.ID), Content: content, ExpectedVersion: request.ExpectedConfigVersion, ExpectedChecksum: request.ExpectedChecksum, MaxReadBytes: 64 * 1024, Deployment: deploymentPlanForDispatch(instance.Deployment)},
ExecutionInput: domain.JobExecutionInput{WorkspaceScope: svc.runtimeProfileScope(instance.ID), Content: content, FileTargetKey: fileTargetKey, ExpectedVersion: request.ExpectedConfigVersion, ExpectedChecksum: request.ExpectedChecksum, MaxReadBytes: 64 * 1024, PluginID: plugin.ID, Deployment: deploymentPlanForDispatch(instance.Deployment)},
IdempotencyKey: request.IdempotencyKey,
Progress: domain.JobProgress{Percent: 0, Message: message},
})
@@ -2337,7 +2382,7 @@ func (svc *CoreService) DispatchFileOperationForSession(sessionID string, reques
}
return domain.CopyFileOperationDispatchResult(domain.FileOperationDispatchResult{
ServerInstanceID: request.ServerInstanceID,
PluginID: request.PluginID,
PluginID: plugin.ID,
Operation: request.Operation,
Key: request.Key,
InputRef: request.InputRef,
+9 -4
View File
@@ -1019,7 +1019,7 @@ func TestPluginFileWorkspaceDoesNotConstrainServerFileDispatch(t *testing.T) {
if err != nil {
t.Fatalf("dispatch declared file read: %v", err)
}
if allowed.Job.TargetKey != "scum-server-settings" || allowed.Job.Capability != domain.JobCapabilityFilesRead || allowed.Job.ExecutionInput.Deployment == nil || allowed.Job.ExecutionInput.Deployment.ServerRoot != `C:\scumserver` {
if allowed.Job.TargetKey != "scum-server-settings" || allowed.Job.ExecutionInput.FileTargetKey != "SCUM/Saved/Config/WindowsServer/ServerSettings.ini" || allowed.Job.Capability != domain.JobCapabilityFilesRead || allowed.Job.ExecutionInput.Deployment == nil || allowed.Job.ExecutionInput.Deployment.ServerRoot != `C:\scumserver` {
t.Fatalf("unexpected declared file dispatch: %+v", allowed)
}
unknown, err := svc.DispatchFileOperationForSession(ownerSession, domain.FileOperationDispatchRequest{
@@ -1313,8 +1313,8 @@ func TestDeclaredFileReadSnapshotProjectionStatesAndPassThroughContent(t *testin
}
createDeclaredFileReadJob(t, svc, instance, endpoint, "job-file-snapshot-cancelled", domain.JobStateCancelled, 3, "")
snapshot, err = svc.GetDeclaredFileReadSnapshotForSession(ownerSession, instance.ID, "scum-server-settings")
if err != nil || snapshot.State != "not-read" {
t.Fatalf("failed/cancelled reads must not mask not-read, snapshot=%+v err=%v", snapshot, err)
if err != nil || snapshot.State != "cancelled" || !strings.Contains(snapshot.Reason, "fixture read cancelled") {
t.Fatalf("failed/cancelled reads must surface the latest Run failure, snapshot=%+v err=%v", snapshot, err)
}
createDeclaredFileReadJob(t, svc, instance, endpoint, "job-file-snapshot-success-old", domain.JobStateSucceeded, 4, "ServerName=Old\nRconPassword=secret\n")
createDeclaredFileReadJob(t, svc, instance, endpoint, "job-file-snapshot-failed-newer", domain.JobStateFailed, 5, "")
@@ -1344,7 +1344,7 @@ func scumTestFileWorkspace() domain.PluginFileWorkspace {
{Key: "scum-logs", Label: "日志文件", Scope: "logs"},
},
Files: []domain.PluginLogicalFile{
{Key: "scum-server-settings", DirectoryKey: "scum-config", Label: "ServerSettings.ini", Kind: "config", Editable: true},
{Key: "scum-server-settings", DirectoryKey: "scum-config", Label: "ServerSettings.ini", Kind: "config", TargetKey: "SCUM/Saved/Config/WindowsServer/ServerSettings.ini", Editable: true},
{Key: "scum-chat-log", DirectoryKey: "scum-logs", Label: "Chat.log", Kind: "log", StreamKey: "scum.chat"},
},
ConfigFields: []domain.PluginConfigField{
@@ -1374,6 +1374,11 @@ func createDeclaredFileReadJob(t *testing.T, svc *CoreService, instance domain.S
}
if state == domain.JobStateSucceeded {
job.ExecutionResult = domain.JobExecutionResult{Kind: "file.read", Version: minuteOffset, Checksum: validator.BytesChecksum([]byte(content)), SizeBytes: int64(len(content)), Content: content}
} else if state == domain.JobStateFailed {
job.Progress.Message = "fixture read failed"
job.ExecutionResult.Summary = "file_read_failed"
} else if state == domain.JobStateCancelled {
job.CancelReason = "fixture read cancelled"
}
if err := svc.store.Jobs().Update(job); err != nil {
t.Fatalf("update declared file read job: %v", err)
+1 -1
View File
@@ -1949,7 +1949,7 @@ func validatePluginFileWorkspace(prefix string, workspace domain.PluginFileWorks
}
for i, item := range workspace.Files {
field := fmt.Sprintf("%s.files[%d]", prefix, i)
if !validDistributionLogicalKey(item.Key) || !directories[item.DirectoryKey] || item.Label == "" || !oneOf(item.Kind, "config", "log") || (item.Kind == "log" && item.StreamKey == "") {
if !validDistributionLogicalKey(item.Key) || !directories[item.DirectoryKey] || item.Label == "" || !oneOf(item.Kind, "config", "log") || (item.Kind == "log" && item.StreamKey == "") || (item.TargetKey != "" && len(validateSafeRelativeRuntimePath(field+".targetKey", item.TargetKey)) > 0) {
violations = append(violations, field+" is invalid")
}
if _, exists := files[item.Key]; exists {
+5
View File
@@ -47,6 +47,11 @@ func TestValidateGamePluginManifestRegistrationValidatesLogicalFileWorkspace(t *
if err := ValidateGamePluginManifestRegistration(registration); err != nil {
t.Fatalf("expected safe logical file workspace: %v", err)
}
registration.Manifest.FileWorkspace.Files[0].TargetKey = "../server-settings.ini"
if err := ValidateGamePluginManifestRegistration(registration); err == nil {
t.Fatal("expected unsafe file target reference to fail")
}
registration.Manifest.FileWorkspace.Files[0].TargetKey = "SCUM/Saved/Config/WindowsServer/ServerSettings.ini"
registration.Manifest.FileWorkspace.Files[0].DirectoryKey = "../host"
if err := ValidateGamePluginManifestRegistration(registration); err == nil {
t.Fatal("expected unsafe logical directory reference to fail")