package service import ( "crypto/sha256" "encoding/hex" "errors" "fmt" "strings" "time" "browser.local/platform/domain" "browser.local/platform/repo" "browser.local/platform/validator" ) func (svc *CoreService) CreateServerInstanceWorkflow(create domain.ServerLifecycleCreate) (domain.ServerLifecycleResult, error) { create = domain.CopyServerLifecycleCreate(create) if err := validator.ValidateServerLifecycleCreate(create); err != nil { return domain.ServerLifecycleResult{}, err } plugin, err := svc.store.GamePlugins().Get(create.PluginID) if err != nil { return domain.ServerLifecycleResult{}, fmt.Errorf("get plugin dependency: %w", err) } if plugin.Status != domain.GamePluginStatusInstalled { return domain.ServerLifecycleResult{}, validationError("plugin must be installed") } create.Deployment = applyPluginCreateDefaults(plugin, create.Deployment) if err := validateLifecycleActionRef(plugin, domain.ServerLifecycleActionCreate); err != nil { return domain.ServerLifecycleResult{}, err } if create.Deployment.Mode != "" { if err := validator.ValidatePluginCreateInputs(plugin.CreateFields, create.Deployment.CreateInputs); err != nil { return domain.ServerLifecycleResult{}, err } } stamp := svc.now() instance := domain.ServerInstance{ ID: create.ID, PluginID: create.PluginID, PluginVersion: plugin.Version, RunEndpointID: create.RunEndpointID, Name: create.Name, OwnerUserID: create.OwnerUserID, State: domain.ServerInstanceStateInstalling, ConfigVersion: 1, ConfigKey: "server.properties", CreatedAt: stamp, UpdatedAt: stamp, Deployment: create.Deployment, } if strings.TrimSpace(create.DeploymentTargetID) != "" { instance.DeploymentTargetID = create.DeploymentTargetID instance.RunEndpointID = dedicatedRunEndpointID(create.ID) instance.State = domain.ServerInstanceStateDraft } if instance.Deployment.Mode != "" { instance.Deployment.ProfileKey = create.ProfileKey instance.Deployment.RuntimeBindings = domain.CopyStringMap(create.Bindings) instance.Deployment.Revision = maxInt(1, instance.Deployment.Revision) instance.Deployment.UpdatedAt = stamp } instance.ConfigContent = buildLogicalServerConfig(instance) instance.ConfigChecksum = validator.BytesChecksum([]byte(instance.ConfigContent)) instance.ConfigUpdatedAt = stamp if strings.TrimSpace(create.DeploymentTargetID) != "" { target, targetErr := svc.GetRunEndpoint(create.DeploymentTargetID) if targetErr != nil { return domain.ServerLifecycleResult{}, fmt.Errorf("get deployment target dependency: %w", targetErr) } if err := svc.validateRunnableEndpoint(target, domain.JobCapabilityDistributionBuild); err != nil { return domain.ServerLifecycleResult{}, err } if err := validator.ValidateServerInstance(instance); err != nil { return domain.ServerLifecycleResult{}, err } var binding domain.RuntimeBinding if strings.TrimSpace(create.ProfileKey) != "" && deploymentNeedsCompleteRuntimeBinding(plugin, instance.Deployment) { var bindingErr error binding, bindingErr = svc.buildRuntimeBinding(instance, plugin, domain.RuntimeBindingUpdate{ProfileKey: create.ProfileKey, Bindings: create.Bindings}, true) if bindingErr != nil { return domain.ServerLifecycleResult{}, bindingErr } } if err := svc.store.ServerInstances().Create(instance); err != nil { return domain.ServerLifecycleResult{}, err } if binding.ID != "" { if err := svc.store.RuntimeBindings().Create(binding); err != nil { return domain.ServerLifecycleResult{}, err } } return domain.CopyServerLifecycleResult(domain.ServerLifecycleResult{Accepted: true, Action: domain.ServerLifecycleActionCreate, Instance: instance}), nil } if strings.TrimSpace(create.RunEndpointID) == "" { instance.State = domain.ServerInstanceStateDraft } if err := validator.ValidateServerInstance(instance); err != nil { return domain.ServerLifecycleResult{}, err } if strings.TrimSpace(create.RunEndpointID) == "" { if err := svc.store.ServerInstances().Create(instance); err != nil { return domain.ServerLifecycleResult{}, err } return domain.CopyServerLifecycleResult(domain.ServerLifecycleResult{Accepted: true, Action: domain.ServerLifecycleActionCreate, Instance: instance}), nil } endpoint, err := svc.GetRunEndpoint(create.RunEndpointID) if err != nil { return domain.ServerLifecycleResult{}, fmt.Errorf("get run endpoint dependency: %w", err) } bootstrapAction := deploymentBootstrapLifecycleAction(plugin, instance) if err := validateLifecycleActionRef(plugin, bootstrapAction); err != nil { return domain.ServerLifecycleResult{}, err } if err := validateServerInstanceLifecycleDependencies(instance, plugin, endpoint, bootstrapAction); err != nil { return domain.ServerLifecycleResult{}, err } if err := svc.validateRunnableEndpoint(endpoint, domain.LifecycleCapabilityForAction(bootstrapAction)); err != nil { return domain.ServerLifecycleResult{}, err } if err := svc.validateLifecycleIdempotency(instance.RunEndpointID, create.IdempotencyKey, instance.ID, domain.LifecycleCapabilityForAction(bootstrapAction)); err != nil { return domain.ServerLifecycleResult{}, err } var binding domain.RuntimeBinding hasBinding := strings.TrimSpace(create.ProfileKey) != "" && deploymentNeedsCompleteRuntimeBinding(plugin, instance.Deployment) if hasBinding { binding, err = svc.buildRuntimeBinding(instance, plugin, domain.RuntimeBindingUpdate{ProfileKey: create.ProfileKey, Bindings: create.Bindings}, true) if err != nil { return domain.ServerLifecycleResult{}, err } } if err := svc.store.ServerInstances().Create(instance); err != nil { return domain.ServerLifecycleResult{}, err } if hasBinding { if err := svc.store.RuntimeBindings().Create(binding); err != nil { return domain.ServerLifecycleResult{}, err } } job, err := svc.dispatchLifecycleJob(instance, bootstrapAction, create.IdempotencyKey) if err != nil { return domain.ServerLifecycleResult{}, err } return domain.CopyServerLifecycleResult(domain.ServerLifecycleResult{ Accepted: true, Action: domain.ServerLifecycleActionCreate, Instance: instance, Job: job, }), nil } func generatedRunEndpointID(serverInstanceID string) string { return "server-run-" + serverInstanceID } // dedicatedRunEndpointID remains a compatibility helper for legacy fixtures. // Production flow uses generatedRunEndpointID only when building a package and // attaches the active endpoint from the first authenticated Run heartbeat. func dedicatedRunEndpointID(serverInstanceID string) string { return generatedRunEndpointID(serverInstanceID) } func runEndpointIDForDistribution(instance domain.ServerInstance) string { if endpointID := strings.TrimSpace(instance.RunEndpointID); endpointID != "" { return endpointID } return generatedRunEndpointID(instance.ID) } func (svc *CoreService) CreateServerInstanceWorkflowForSession(sessionID string, create domain.ServerLifecycleCreate) (domain.ServerLifecycleResult, error) { user, err := svc.GetCurrentUser(sessionID) if err != nil { return domain.ServerLifecycleResult{}, err } if strings.TrimSpace(create.OwnerUserID) == "" { create.OwnerUserID = user.ID } if !isPlatformAdmin(user) && create.OwnerUserID != user.ID { return domain.ServerLifecycleResult{}, ErrForbidden } return svc.CreateServerInstanceWorkflow(create) } func (svc *CoreService) StartServerInstance(command domain.ServerLifecycleCommand) (domain.ServerLifecycleResult, error) { return svc.dispatchExistingServerLifecycle(command, domain.ServerLifecycleActionStart, []domain.ServerInstanceState{ domain.ServerInstanceStateReady, domain.ServerInstanceStateStopped, domain.ServerInstanceStateFailed, }) } func (svc *CoreService) StartServerInstanceForSession(sessionID string, command domain.ServerLifecycleCommand) (domain.ServerLifecycleResult, error) { if err := svc.authorizeServerLifecycle(sessionID, command.ServerInstanceID); err != nil { return domain.ServerLifecycleResult{}, err } return svc.StartServerInstance(command) } func (svc *CoreService) StopServerInstance(command domain.ServerLifecycleCommand) (domain.ServerLifecycleResult, error) { return svc.dispatchExistingServerLifecycle(command, domain.ServerLifecycleActionStop, []domain.ServerInstanceState{ domain.ServerInstanceStateRunning, }) } func (svc *CoreService) StopServerInstanceForSession(sessionID string, command domain.ServerLifecycleCommand) (domain.ServerLifecycleResult, error) { if err := svc.authorizeServerLifecycle(sessionID, command.ServerInstanceID); err != nil { return domain.ServerLifecycleResult{}, err } return svc.StopServerInstance(command) } func (svc *CoreService) QueryServerInstanceProcessForSession(sessionID string, command domain.ServerLifecycleCommand) (domain.ServerLifecycleResult, error) { if err := svc.authorizeServerLifecycle(sessionID, command.ServerInstanceID); err != nil { return domain.ServerLifecycleResult{}, err } return svc.dispatchExistingServerLifecycle(command, domain.ServerLifecycleActionStatus, []domain.ServerInstanceState{ domain.ServerInstanceStateReady, domain.ServerInstanceStateStopped, domain.ServerInstanceStateRunning, domain.ServerInstanceStateFailed, }) } func (svc *CoreService) dispatchExistingServerLifecycle(command domain.ServerLifecycleCommand, action domain.ServerLifecycleAction, allowedStates []domain.ServerInstanceState) (domain.ServerLifecycleResult, error) { command = domain.CopyServerLifecycleCommand(command) if err := validator.ValidateServerLifecycleCommand(command); err != nil { return domain.ServerLifecycleResult{}, err } if err := validator.ValidateServerLifecycleAction(action); err != nil { return domain.ServerLifecycleResult{}, err } instance, err := svc.store.ServerInstances().Get(command.ServerInstanceID) if err != nil { return domain.ServerLifecycleResult{}, err } if instance.ConfigVersion != command.ExpectedConfigVersion { return domain.ServerLifecycleResult{}, validationError("expectedConfigVersion must match server instance") } if !serverStateAllowed(instance.State, allowedStates) { return domain.ServerLifecycleResult{}, validationError(fmt.Sprintf("server instance state %q cannot %s", instance.State, action)) } plugin, endpoint, err := svc.lifecycleDependencies(instance.PluginID, instance.RunEndpointID) if err != nil { return domain.ServerLifecycleResult{}, err } if err := validateLifecycleActionRef(plugin, action); err != nil { return domain.ServerLifecycleResult{}, err } if err := validateServerInstanceLifecycleDependencies(instance, plugin, endpoint, action); err != nil { return domain.ServerLifecycleResult{}, err } if action == domain.ServerLifecycleActionStart { binding, bindingErr := svc.runtimeBindingForServer(instance.ID) if bindingErr != nil && !errors.Is(bindingErr, repo.ErrNotFound) { return domain.ServerLifecycleResult{}, bindingErr } if bindingErr == nil { if profile, exists := runtimeLifecycleProfileForKey(plugin.RuntimeProfiles, binding.ProfileKey); exists && len(profile.DLLExtensionRefs) > 0 { if _, extensionErr := lifecycleDLLExtensionPlans(plugin.RuntimeProfiles, profile, endpoint); extensionErr != nil { return domain.ServerLifecycleResult{}, extensionErr } } } } if err := svc.validateRunnableEndpoint(endpoint, domain.LifecycleCapabilityForAction(action)); err != nil { return domain.ServerLifecycleResult{}, err } if action == domain.ServerLifecycleActionStart { instance.LifecycleProcessID = "" instance.LifecycleObservationSeq = 0 instance.LifecycleObservedAt = time.Time{} if err := svc.store.ServerInstances().Update(instance); err != nil { return domain.ServerLifecycleResult{}, err } } job, err := svc.dispatchLifecycleJob(instance, action, command.IdempotencyKey) if err != nil { return domain.ServerLifecycleResult{}, err } return domain.CopyServerLifecycleResult(domain.ServerLifecycleResult{ Accepted: true, Action: action, Instance: instance, Job: job, }), nil } func (svc *CoreService) lifecycleDependencies(pluginID string, runEndpointID string) (domain.GamePlugin, domain.RunEndpoint, error) { plugin, err := svc.store.GamePlugins().Get(pluginID) if err != nil { return domain.GamePlugin{}, domain.RunEndpoint{}, fmt.Errorf("get plugin dependency: %w", err) } endpoint, err := svc.GetRunEndpoint(runEndpointID) if err != nil { return domain.GamePlugin{}, domain.RunEndpoint{}, fmt.Errorf("get run endpoint dependency: %w", err) } return plugin, endpoint, nil } func (svc *CoreService) dispatchLifecycleJob(instance domain.ServerInstance, action domain.ServerLifecycleAction, idempotencyKey string) (domain.Job, error) { capability := domain.LifecycleCapabilityForAction(action) binding, err := svc.runtimeBindingForServer(instance.ID) if err != nil && !errors.Is(err, repo.ErrNotFound) { return domain.Job{}, err } plugin, err := svc.store.GamePlugins().Get(instance.PluginID) if err != nil { return domain.Job{}, err } profileKey := "" if err == nil && strings.TrimSpace(binding.ProfileKey) != "" { profileKey = binding.ProfileKey } else { profileKey = lifecycleDefaultProfileKey(instance, plugin, instance.Deployment.ProfileKey) } actionRef := "" profile, hasProfile := runtimeLifecycleProfileForKey(plugin.RuntimeProfiles, profileKey) if hasProfile { if ref := runtimeProfileActionRef(profile.ActionRefs, action); ref != "" { actionRef = ref } } if strings.TrimSpace(actionRef) == "" { actionRef = lifecycleActionRef(plugin, action) } if strings.TrimSpace(actionRef) == "" { return domain.Job{}, validationError(fmt.Sprintf("plugin %s lifecycle action is required", action)) } var dllExtensions []domain.RuntimeDLLExtensionPlan var logSources []domain.RuntimeLogSource if action == domain.ServerLifecycleActionStart && hasProfile { endpoint, err := svc.GetRunEndpoint(instance.RunEndpointID) if err != nil { return domain.Job{}, err } dllExtensions, err = lifecycleDLLExtensionPlans(plugin.RuntimeProfiles, profile, endpoint) if err != nil { return domain.Job{}, err } logSources = lifecycleProcessLogSources(plugin.RuntimeProfiles) } job, err := svc.CreateJob(domain.Job{ ID: lifecycleJobID(instance.ID, action, idempotencyKey), ServerInstanceID: instance.ID, RunEndpointID: instance.RunEndpointID, Capability: capability, TargetKey: actionRef, IdempotencyKey: idempotencyKey, Progress: lifecycleJobProgress(instance.Deployment), ExecutionInput: domain.JobExecutionInput{ WorkspaceScope: profileKey, PluginID: plugin.ID, LifecycleOperation: lifecycleExecutionOperation(action), LogSources: logSources, DLLExtensions: dllExtensions, Deployment: deploymentPlanForDispatch(instance.Deployment), }, }) if err != nil { return domain.Job{}, err } if job.ServerInstanceID != instance.ID || job.RunEndpointID != instance.RunEndpointID || job.Capability != capability { return domain.Job{}, validationError("idempotencyKey is already used for a different lifecycle target") } return job, nil } func lifecycleProcessLogSources(profiles domain.GamePluginRuntimeProfiles) []domain.RuntimeLogSource { sources := []domain.RuntimeLogSource{} for _, source := range profiles.LogSources { if source.Kind == "process.stdout" || source.Kind == "process.stderr" { sources = append(sources, source) } } return sources } func deploymentBootstrapLifecycleAction(plugin domain.GamePlugin, instance domain.ServerInstance) domain.ServerLifecycleAction { if instance.Deployment.Mode != domain.ServerDeploymentModeGuided { return domain.ServerLifecycleActionCreate } profile, exists := runtimeLifecycleProfileForKey(plugin.RuntimeProfiles, instance.Deployment.ProfileKey) if !exists || !containsString(profile.Capabilities, domain.LifecycleCapabilityStart) { return domain.ServerLifecycleActionCreate } if strings.TrimSpace(runtimeProfileActionRef(profile.ActionRefs, domain.ServerLifecycleActionStart)) == "" && strings.TrimSpace(plugin.LifecycleActions.Start) == "" { return domain.ServerLifecycleActionCreate } return domain.ServerLifecycleActionStart } func lifecycleJobProgress(deployment domain.ServerDeploymentDefinition) domain.JobProgress { if deployment.Mode != "" { return domain.JobProgress{Percent: 0, Phase: "queued", Message: "deployment queued; awaiting Run claim"} } return domain.JobProgress{} } func deploymentPlanForDispatch(definition domain.ServerDeploymentDefinition) *domain.ServerDeploymentDefinition { if definition.Mode == "" { return nil } copy := domain.CopyServerDeploymentDefinition(definition) return © } func lifecycleExecutionOperation(action domain.ServerLifecycleAction) string { switch action { case domain.ServerLifecycleActionCreate: return "install" case domain.ServerLifecycleActionStart: return "start" case domain.ServerLifecycleActionStop: return "stop" case domain.ServerLifecycleActionStatus: return "status" default: return string(action) } } func runtimeProfileActionRef(actions domain.PluginLifecycleActions, action domain.ServerLifecycleAction) string { switch action { case domain.ServerLifecycleActionCreate: return actions.Install case domain.ServerLifecycleActionStart: return actions.Start case domain.ServerLifecycleActionStop: return actions.Stop case domain.ServerLifecycleActionStatus: return actions.Status default: return "" } } func runtimeLifecycleProfileForKey(profiles domain.GamePluginRuntimeProfiles, key string) (domain.RuntimeLifecycleProfile, bool) { return runtimeLifecycleProfile(profiles, key) } func lifecycleDLLExtensionPlans(profiles domain.GamePluginRuntimeProfiles, profile domain.RuntimeLifecycleProfile, endpoint domain.RunEndpoint) ([]domain.RuntimeDLLExtensionPlan, error) { if len(profile.DLLExtensionRefs) == 0 { return nil, nil } byKey := make(map[string]domain.RuntimeDLLExtensionProfile, len(profiles.DLLExtensions)) for _, extension := range profiles.DLLExtensions { byKey[extension.Key] = extension } plans := make([]domain.RuntimeDLLExtensionPlan, 0, len(profile.DLLExtensionRefs)) for _, key := range profile.DLLExtensionRefs { extension, exists := byKey[key] if !exists || extension.ReleaseState != "ready" { return nil, validationError("extension_release_unavailable: selected DLL release is not ready") } if !runtimeDLLExtensionSupportsTarget(extension, endpoint.Platform, endpoint.Architecture) { return nil, validationError("unsupported_extension_platform: UE4SS DLL requires windows/amd64") } plans = append(plans, domain.RuntimeDLLExtensionPlan{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}) } return plans, nil } func runtimeDLLExtensionSupportsTarget(extension domain.RuntimeDLLExtensionProfile, platform string, architecture string) bool { for _, target := range extension.SupportedTargets { if strings.EqualFold(target.OS, platform) && strings.EqualFold(target.Arch, architecture) { return true } } return false } func (svc *CoreService) validateLifecycleIdempotency(runEndpointID string, idempotencyKey string, serverInstanceID string, capability string) error { existing, err := svc.store.Jobs().GetByIdempotency(runEndpointID, idempotencyKey) if errors.Is(err, repo.ErrNotFound) { return nil } if err != nil { return err } if existing.ServerInstanceID == serverInstanceID && existing.Capability == capability { return nil } return validationError("idempotencyKey is already used for a different lifecycle target") } func validateLifecycleActionRef(plugin domain.GamePlugin, action domain.ServerLifecycleAction) error { if strings.TrimSpace(lifecycleActionRef(plugin, action)) == "" { return validationError(fmt.Sprintf("plugin %s lifecycle action is required", action)) } return nil } func lifecycleActionRef(plugin domain.GamePlugin, action domain.ServerLifecycleAction) string { switch action { case domain.ServerLifecycleActionCreate: return plugin.LifecycleActions.Install case domain.ServerLifecycleActionStart: return plugin.LifecycleActions.Start case domain.ServerLifecycleActionStop: return plugin.LifecycleActions.Stop case domain.ServerLifecycleActionStatus: return plugin.LifecycleActions.Status default: return "" } } func serverStateAllowed(state domain.ServerInstanceState, allowed []domain.ServerInstanceState) bool { for _, candidate := range allowed { if state == candidate { return true } } return false } func lifecycleJobID(serverInstanceID string, action domain.ServerLifecycleAction, idempotencyKey string) string { sum := sha256.Sum256([]byte(idempotencyKey)) return fmt.Sprintf("server-lifecycle:%s:%s:%s", serverInstanceID, action, hex.EncodeToString(sum[:8])) } func validateServerInstanceLifecycleDependencies(instance domain.ServerInstance, plugin domain.GamePlugin, endpoint domain.RunEndpoint, action domain.ServerLifecycleAction) error { if requiredShellCapability := deploymentShellCapability(instance.Deployment.Shell); requiredShellCapability != "" && !containsString(endpoint.Capabilities, requiredShellCapability) { return validationError("run endpoint policy does not allow selected command shell") } return validator.ValidateServerInstanceDependenciesForCapabilities(instance, plugin, endpoint, lifecycleRequiredRunCapabilities(instance, plugin, action)) } func lifecycleRequiredRunCapabilities(instance domain.ServerInstance, plugin domain.GamePlugin, action domain.ServerLifecycleAction) []string { required := append([]string(nil), domain.LifecycleCapabilityForAction(action)) if instance.Deployment.Mode == domain.ServerDeploymentModeCustom { required = append(required, domain.JobCapabilityDeploymentPlan) } return compactUniqueStrings(required) } func compactUniqueStrings(values []string) []string { out := make([]string, 0, len(values)) for _, value := range values { value = strings.TrimSpace(value) if value == "" || containsString(out, value) { continue } out = append(out, value) } return out }