package service import ( "crypto/sha256" "encoding/hex" "errors" "fmt" "strings" "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, endpoint, err := svc.lifecycleDependencies(create.PluginID, create.RunEndpointID) if err != nil { return domain.ServerLifecycleResult{}, err } if err := validateLifecycleActionRef(plugin, domain.ServerLifecycleActionCreate); 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, } instance.ConfigContent = buildLogicalServerConfig(instance) instance.ConfigChecksum = validator.BytesChecksum([]byte(instance.ConfigContent)) instance.ConfigUpdatedAt = stamp if err := validator.ValidateServerInstance(instance); err != nil { return domain.ServerLifecycleResult{}, err } if err := validator.ValidateServerInstanceDependencies(instance, plugin, endpoint); err != nil { return domain.ServerLifecycleResult{}, err } if err := svc.validateRunnableEndpoint(endpoint, domain.LifecycleCapabilityForAction(domain.ServerLifecycleActionCreate)); err != nil { return domain.ServerLifecycleResult{}, err } if err := svc.validateLifecycleIdempotency(instance.RunEndpointID, create.IdempotencyKey, instance.ID, domain.LifecycleCapabilityForAction(domain.ServerLifecycleActionCreate)); err != nil { return domain.ServerLifecycleResult{}, err } 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 err := svc.store.RuntimeBindings().Create(binding); err != nil { return domain.ServerLifecycleResult{}, err } job, err := svc.dispatchLifecycleJob(instance, domain.ServerLifecycleActionCreate, 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 (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, }) } 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 := validator.ValidateServerInstanceDependencies(instance, plugin, endpoint); 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.requireCompleteRuntimeBindings(instance.OwnerUserID, instance.ID, "server.lifecycle."+string(action)+".denied"); err != nil { return domain.ServerLifecycleResult{}, err } if err := svc.validateRunnableEndpoint(endpoint, domain.LifecycleCapabilityForAction(action)); 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.store.RunEndpoints().Get(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 { return domain.Job{}, err } plugin, err := svc.store.GamePlugins().Get(instance.PluginID) if err != nil { return domain.Job{}, err } actionRef := binding.ProfileKey profile, hasProfile := runtimeLifecycleProfileForKey(plugin.RuntimeProfiles, binding.ProfileKey) if hasProfile { if ref := runtimeProfileActionRef(profile.ActionRefs, action); ref != "" { actionRef = ref } } else if ref := lifecycleActionRef(plugin, action); ref != "" { actionRef = ref } if strings.TrimSpace(actionRef) == "" { return domain.Job{}, validationError(fmt.Sprintf("plugin %s lifecycle action is required", action)) } var dllExtensions []domain.RuntimeDLLExtensionPlan if action == domain.ServerLifecycleActionStart && hasProfile { endpoint, err := svc.store.RunEndpoints().Get(instance.RunEndpointID) if err != nil { return domain.Job{}, err } dllExtensions, err = lifecycleDLLExtensionPlans(plugin.RuntimeProfiles, profile, endpoint) if err != nil { return domain.Job{}, err } } job, err := svc.CreateJob(domain.Job{ ID: lifecycleJobID(instance.ID, action, idempotencyKey), ServerInstanceID: instance.ID, RunEndpointID: instance.RunEndpointID, Capability: capability, TargetKey: actionRef, IdempotencyKey: idempotencyKey, ExecutionInput: domain.JobExecutionInput{ WorkspaceScope: binding.ProfileKey, PluginID: plugin.ID, LifecycleOperation: lifecycleExecutionOperation(action), DLLExtensions: dllExtensions, }, }) 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 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) { for _, profile := range profiles.LifecycleProfiles { if profile.Key == key { return profile, true } } return domain.RuntimeLifecycleProfile{}, false } 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])) }