package service import ( "strings" "time" "browser.local/platform/domain" "browser.local/platform/validator" ) func (svc *CoreService) ReportRunLifecycle(report domain.RunLifecycleReport) (domain.RunLifecycleReportResult, error) { report = domain.CopyRunLifecycleReport(report) if err := validator.ValidateRunLifecycleReport(report); err != nil { return domain.RunLifecycleReportResult{}, err } if _, err := svc.validatedRunSession(report.RunEndpointID, report.SessionToken); err != nil { return domain.RunLifecycleReportResult{}, err } instance, err := svc.store.ServerInstances().Get(report.ServerInstanceID) if err != nil { return domain.RunLifecycleReportResult{}, err } if instance.RunEndpointID != report.RunEndpointID { return domain.RunLifecycleReportResult{}, validationError("runEndpointId must match server instance") } if instance.State == domain.ServerInstanceStateDeleted { return domain.RunLifecycleReportResult{}, validationError("server instance must not be deleted") } stamp := svc.now() nextState, projected := lifecycleProjectedState(report.Capability, report.State, report.ExecutionResult) if lifecycleObservationIsStale(instance, report) { projected = false nextState = instance.State } if projected { instance.State = nextState if report.ManagedProcessID != "" { instance.LifecycleProcessID = report.ManagedProcessID instance.LifecycleObservationSeq = report.ObservationSeq instance.LifecycleObservedAt = report.ObservedAt if instance.LifecycleObservedAt.IsZero() { instance.LifecycleObservedAt = stamp } } instance.UpdatedAt = stamp if err := validator.ValidateServerInstance(instance); err != nil { return domain.RunLifecycleReportResult{}, err } if err := svc.store.ServerInstances().Update(instance); err != nil { return domain.RunLifecycleReportResult{}, err } svc.publishLogProcessState(instance) } return domain.CopyRunLifecycleReportResult(domain.RunLifecycleReportResult{Accepted: true, RunEndpointID: report.RunEndpointID, ServerInstanceID: report.ServerInstanceID, ProjectedState: nextState, ServerTime: stamp}), nil } func lifecycleObservationIsStale(instance domain.ServerInstance, report domain.RunLifecycleReport) bool { if report.ManagedProcessID == "" || instance.LifecycleProcessID == "" { return false } if report.ManagedProcessID == instance.LifecycleProcessID { return report.ObservationSeq <= instance.LifecycleObservationSeq } return !report.ObservedAt.IsZero() && !instance.LifecycleObservedAt.IsZero() && report.ObservedAt.Before(instance.LifecycleObservedAt) } func (svc *CoreService) projectRemoteAdapterJobResult(job domain.Job, stamp time.Time) error { if !strings.HasPrefix(job.Capability, "remote.") || job.ServerInstanceID == "" || !isTerminalJobState(job.State) { return nil } return nil } func (svc *CoreService) projectLifecycleJobResult(job domain.Job, stamp time.Time) error { if job.Capability == domain.JobCapabilityConfigWrite { if job.State != domain.JobStateSucceeded { return nil } instance, err := svc.store.ServerInstances().Get(job.ServerInstanceID) if err != nil { return err } instance.ConfigKey = job.TargetKey instance.ConfigContent = job.ExecutionInput.Content instance.ConfigChecksum = job.ExecutionResult.Checksum instance.ConfigVersion = job.ExecutionResult.Version instance.ConfigUpdatedAt = stamp instance.UpdatedAt = stamp if err := validator.ValidateServerInstance(instance); err != nil { return err } if err := svc.store.ServerInstances().Update(instance); err != nil { return err } return nil } nextState, ok := lifecycleProjectedState(job.Capability, job.State, job.ExecutionResult) if !ok || job.ServerInstanceID == "" { return nil } instance, err := svc.store.ServerInstances().Get(job.ServerInstanceID) if err != nil { return err } instance.State = nextState instance.UpdatedAt = stamp if err := validator.ValidateServerInstance(instance); err != nil { return err } if err := svc.store.ServerInstances().Update(instance); err != nil { return err } svc.publishLogProcessState(instance) return nil } func (svc *CoreService) projectServerDeploymentProgress(job domain.Job, stamp time.Time) error { if job.ExecutionInput.Deployment == nil || job.ServerInstanceID == "" { return nil } instance, err := svc.store.ServerInstances().Get(job.ServerInstanceID) if err != nil { return err } projection := domain.CopyServerDeploymentProjection(instance.DeploymentProjection) projection.State = "running" projection.Operation = job.ExecutionInput.LifecycleOperation switch job.Progress.Phase { case "preflight": projection.PreflightState = "running" case "scan", "discover", "discovery": projection.DiscoveryState = "running" case "install", "configure", "mapping": projection.MappingState = "running" case "start", "health": projection.VerificationState = "running" } projection.UpdatedAt = stamp instance.DeploymentProjection = projection instance.UpdatedAt = stamp return svc.store.ServerInstances().Update(instance) } func lifecycleProjectedState(capability string, jobState domain.JobState, result domain.JobExecutionResult) (domain.ServerInstanceState, bool) { if capability != domain.LifecycleCapabilityInstall && capability != domain.LifecycleCapabilityStart && capability != domain.LifecycleCapabilityStop && capability != domain.LifecycleCapabilityStatus { return "", false } if capability == domain.LifecycleCapabilityStatus { if jobState != domain.JobStateSucceeded { return "", false } return lifecycleStateFromProcessResult(result) } if jobState == domain.JobStateFailed || jobState == domain.JobStateCancelled { return domain.ServerInstanceStateFailed, true } if jobState != domain.JobStateSucceeded { return "", false } switch capability { case domain.LifecycleCapabilityInstall: return domain.ServerInstanceStateReady, true case domain.LifecycleCapabilityStart: return lifecycleStateFromProcessResult(result) case domain.LifecycleCapabilityStop: if state, ok := lifecycleStateFromProcessResult(result); ok { return state, true } return domain.ServerInstanceStateStopped, true default: return "", false } } func lifecycleStateFromProcessResult(result domain.JobExecutionResult) (domain.ServerInstanceState, bool) { switch strings.TrimSpace(result.ProcessState) { case "running": return domain.ServerInstanceStateRunning, true case "stopped", "not-started": return domain.ServerInstanceStateStopped, true case "exited": switch strings.TrimSpace(result.ExitClassification) { case "requested-stop", "forced-stop", "already-stopped": return domain.ServerInstanceStateStopped, true default: return domain.ServerInstanceStateFailed, true } default: return "", false } }