Stream plugin file-tail logs through Run plan
This commit is contained in:
@@ -59,7 +59,7 @@ Job ack, progress, cancellation polling, reconciliation, and terminal result cal
|
||||
|
||||
## Generated Run autonomous lifecycle plan
|
||||
|
||||
Platform-owned Run distribution builds embed an autonomous lifecycle plan for the server-scoped generated Run. The plan carries the server/plugin identity, selected runtime profile, target OS/architecture/release, plugin lifecycle action refs, dependency probes/install plans, process log sources, optional DLL extension plans, runtime bindings, and redacted deployment inputs. The builder includes the same JSON as internal build input and as `.platform/autonomous-lifecycle-plan.json` in the generated workspace seed. Run reads this package-local plan on startup and performs plugin-declared init, dependency verification/install, install-if-needed, readiness/status, and start behavior locally before reporting observed state back to Platform.
|
||||
Platform-owned Run distribution builds embed an autonomous lifecycle plan for the server-scoped generated Run. The plan carries the server/plugin identity, selected runtime profile, target OS/architecture/release, plugin lifecycle action refs, dependency probes/install plans, plugin-declared runtime log sources such as supervised process channels and file tails, optional DLL extension plans, runtime bindings, and redacted deployment inputs. The builder includes the same JSON as internal build input and as `.platform/autonomous-lifecycle-plan.json` in the generated workspace seed. Run reads this package-local plan on startup and performs plugin-declared init, dependency verification/install, install-if-needed, readiness/status, and start behavior locally before reporting observed state back to Platform.
|
||||
|
||||
The plan is build input for the generated package, not a machine-side job-channel payload. Generated Run registration must not be treated as a trigger to enqueue `process.start`, `process.install`, or `process.status` work; Platform state converges from Run heartbeats, logs, lifecycle reports, supervised process facts, and terminal job/report messages. Platform and Run must not add game-specific hardcoding to interpret the plan.
|
||||
|
||||
|
||||
@@ -264,7 +264,7 @@ func runAutonomousLifecyclePlan(distribution domain.RunDistribution, instance do
|
||||
}
|
||||
}
|
||||
for _, source := range plugin.RuntimeProfiles.LogSources {
|
||||
if source.Kind == "process.stdout" || source.Kind == "process.stderr" {
|
||||
if autonomousRunLogSourceKind(source.Kind) {
|
||||
plan.LogSources = append(plan.LogSources, autonomousLogSource(source))
|
||||
}
|
||||
}
|
||||
@@ -318,6 +318,10 @@ func autonomousLogSource(source domain.RuntimeLogSource) domain.RunAutonomousLog
|
||||
return domain.RunAutonomousLogSource{Key: source.Key, Kind: source.Kind, TargetKey: source.TargetKey, StreamKey: source.StreamKey, CursorKind: source.CursorKind, RetentionDays: source.RetentionDays}
|
||||
}
|
||||
|
||||
func autonomousRunLogSourceKind(kind string) bool {
|
||||
return kind == "process.stdout" || kind == "process.stderr" || kind == "file.tail"
|
||||
}
|
||||
|
||||
func autonomousDLLExtension(extension domain.RuntimeDLLExtensionPlan) domain.RunAutonomousDLLExtension {
|
||||
return domain.RunAutonomousDLLExtension{Key: extension.Key, Version: extension.Version, ReleaseURL: extension.ReleaseURL, Checksum: extension.Checksum, SizeBytes: extension.SizeBytes, TargetKey: extension.TargetKey, ModKey: extension.ModKey, DLLRef: extension.DLLRef, SCUMExecutableChecksum: extension.SCUMExecutableChecksum, UE4SSABI: extension.UE4SSABI, RCONPort: extension.RCONPort}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,10 @@ func TestCoreServiceKeepsPlatformBuildKeyOffMachineJobChannel(t *testing.T) {
|
||||
{Path: "actions/install.json", Content: `{"version":1,"action":"install","mode":"oneshot"}`, Mode: 0o600},
|
||||
{Path: "bin/install-server", Content: "#!/usr/bin/env sh\n", Mode: 0o700},
|
||||
}
|
||||
plugin.RuntimeProfiles.LogSources = append(plugin.RuntimeProfiles.LogSources, domain.RuntimeLogSource{Key: "console", Kind: "process.stdout", TargetKey: "server/process", StreamKey: "console", CursorKind: "sequence", RetentionDays: 14})
|
||||
plugin.RuntimeProfiles.LogSources = append(plugin.RuntimeProfiles.LogSources,
|
||||
domain.RuntimeLogSource{Key: "console", Kind: "process.stdout", TargetKey: "server/process", StreamKey: "console", CursorKind: "sequence", RetentionDays: 14},
|
||||
domain.RuntimeLogSource{Key: "server-events", Kind: "file.tail", TargetKey: "logs/server", StreamKey: "scum.server", CursorKind: "fingerprint", RetentionDays: 90},
|
||||
)
|
||||
if err := svc.store.GamePlugins().Update(plugin); err != nil {
|
||||
t.Fatalf("seed plugin lifecycle assets: %v", err)
|
||||
}
|
||||
@@ -87,7 +90,7 @@ func TestCoreServiceKeepsPlatformBuildKeyOffMachineJobChannel(t *testing.T) {
|
||||
if plan == nil || plan.SchemaVersion != "1" || plan.ServerInstanceID != instance.ID || plan.PluginID != plugin.ID || plan.ProfileKey != "local" || plan.Bootstrap == nil || plan.Bootstrap.Action != domain.ServerLifecycleActionStart || plan.Bootstrap.TargetKey != "actions/start.json" {
|
||||
t.Fatalf("platform builder received incomplete autonomous lifecycle plan: %+v", plan)
|
||||
}
|
||||
if len(plan.DependencyProbes) != 1 || plan.DependencyProbes[0].Key != "java-runtime" || len(plan.InstallPlans) != 1 || plan.InstallPlans[0].Key != "java-install" || len(plan.LogSources) != 1 || plan.LogSources[0].Kind != "process.stdout" || plan.RuntimeBindings["logs/latest"] != "runtime.logs.latest" {
|
||||
if len(plan.DependencyProbes) != 1 || plan.DependencyProbes[0].Key != "java-runtime" || len(plan.InstallPlans) != 1 || plan.InstallPlans[0].Key != "java-install" || len(plan.LogSources) != 3 || !hasAutonomousLogSource(plan.LogSources, "process.stdout", "console") || !hasAutonomousLogSource(plan.LogSources, "file.tail", "latest-log") || !hasAutonomousLogSource(plan.LogSources, "file.tail", "scum.server") || plan.RuntimeBindings["logs/latest"] != "runtime.logs.latest" {
|
||||
t.Fatalf("autonomous lifecycle plan lost plugin runtime declarations: %+v", plan)
|
||||
}
|
||||
var seededPlan domain.RunAutonomousLifecyclePlan
|
||||
@@ -143,6 +146,15 @@ func TestCoreServiceKeepsPlatformBuildKeyOffMachineJobChannel(t *testing.T) {
|
||||
completeDistributionBuild(t, svc, distribution, nil)
|
||||
}
|
||||
|
||||
func hasAutonomousLogSource(sources []domain.RunAutonomousLogSource, kind string, streamKey string) bool {
|
||||
for _, source := range sources {
|
||||
if source.Kind == kind && source.StreamKey == streamKey {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestCoreServiceBuildsWithoutRegisteredDistributionWorker(t *testing.T) {
|
||||
svc, session, instance := newDistributionTestFixture(t)
|
||||
bootstrapEndpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID)
|
||||
|
||||
@@ -130,7 +130,7 @@ func (svc *CoreService) ensureLogStreamForBatch(batch domain.LogBatchIngest, sta
|
||||
}
|
||||
|
||||
func (svc *CoreService) ensureRunLogStreamForBatch(batch domain.LogBatchIngest, stamp time.Time) error {
|
||||
if batch.Source != domain.LogStreamSourceProcess && batch.Source != domain.LogStreamSourceManagementProgram {
|
||||
if batch.Source != domain.LogStreamSourceProcess && batch.Source != domain.LogStreamSourceFile && batch.Source != domain.LogStreamSourceManagementProgram {
|
||||
return repo.ErrNotFound
|
||||
}
|
||||
if batch.LogStreamID != runLogStreamID(batch.RunEndpointID, batch.ServerInstanceID, batch.StreamKey) && !legacyAutonomousLogStream(batch) {
|
||||
|
||||
@@ -285,6 +285,42 @@ func TestCoreServiceAcceptsAutonomousRunLogStreamWithoutPlatformJob(t *testing.T
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceAcceptsAutonomousRunFileTailLogStreamWithoutPlatformJob(t *testing.T) {
|
||||
svc, sessionToken := newRegisteredLogIngestService(t)
|
||||
entry := domain.LogEntry{Seq: 1, Timestamp: time.Date(2026, 7, 3, 12, 0, 1, 0, time.UTC), Level: "info", Line: "LogSCUM: Display: player joined"}
|
||||
streamID := runLogStreamID("run-local", "server-1", "scum.server")
|
||||
ack, err := svc.IngestLogBatch(domain.LogBatchIngest{
|
||||
RunEndpointID: "run-local",
|
||||
SessionToken: sessionToken,
|
||||
LogStreamID: streamID,
|
||||
ServerInstanceID: "server-1",
|
||||
StreamKey: "scum.server",
|
||||
Source: domain.LogStreamSourceFile,
|
||||
FirstSeq: entry.Seq,
|
||||
LastSeq: entry.Seq,
|
||||
Compression: "none",
|
||||
Checksum: validator.LogLineChecksum(entry.Line),
|
||||
Entries: []domain.LogEntry{entry},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ingest autonomous run file-tail log batch: %v", err)
|
||||
}
|
||||
if !ack.Accepted || ack.LogStreamID != streamID || ack.LatestSeq != entry.Seq {
|
||||
t.Fatalf("unexpected autonomous file-tail stream ack: %+v", ack)
|
||||
}
|
||||
stream, err := svc.GetLogStream(streamID)
|
||||
if err != nil {
|
||||
t.Fatalf("get autonomous file-tail stream: %v", err)
|
||||
}
|
||||
if stream.ServerInstanceID != "server-1" || stream.StreamKey != "scum.server" || stream.Source != domain.LogStreamSourceFile {
|
||||
t.Fatalf("unexpected autonomous file-tail stream metadata: %+v", stream)
|
||||
}
|
||||
query, err := svc.QueryLogStream(domain.LogStreamCursorQuery{LogStreamID: streamID, AfterSeq: 0, Limit: 10})
|
||||
if err != nil || len(query.Entries) != 1 || query.Entries[0].Line != entry.Line {
|
||||
t.Fatalf("unexpected autonomous file-tail query: %+v err=%v", query, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreServiceAcceptsLegacyAutonomousJobLogStreamWithoutPlatformJob(t *testing.T) {
|
||||
svc, sessionToken := newRegisteredLogIngestService(t)
|
||||
entry := domain.LogEntry{Seq: 1, Timestamp: time.Date(2026, 7, 3, 12, 0, 1, 0, time.UTC), Level: "info", Line: "legacy autonomous bootstrap output"}
|
||||
|
||||
Reference in New Issue
Block a user