Remove legacy runtime deployment paths
This commit is contained in:
@@ -37,5 +37,5 @@ PLATFORM_BUILDER_SOURCE_REPOSITORY=git@git.npc0.com:admin343/run.git
|
|||||||
PLATFORM_BUILDER_SOURCE_REVISION=main
|
PLATFORM_BUILDER_SOURCE_REVISION=main
|
||||||
PLATFORM_BUILDER_WORKSPACE_DIR=.platform-data/distribution-builds
|
PLATFORM_BUILDER_WORKSPACE_DIR=.platform-data/distribution-builds
|
||||||
PLATFORM_BUILDER_TIMEOUT_SECONDS=1800
|
PLATFORM_BUILDER_TIMEOUT_SECONDS=1800
|
||||||
# URL embedded into generated Run and client-manager packages.
|
# URL embedded into generated Run packages; override for tunnel or production access.
|
||||||
PLATFORM_RUN_RELEASE_URL=https://scum.npc0.com
|
PLATFORM_RUN_RELEASE_URL=http://127.0.0.1:8080
|
||||||
|
|||||||
+1
-1
@@ -62,7 +62,7 @@ Runtime configuration:
|
|||||||
- `PLATFORM_BUILDER_WORKSPACE_DIR`: private per-plugin/per-job build workspace, default `<PLATFORM_DATA_DIR>/distribution-builds`.
|
- `PLATFORM_BUILDER_WORKSPACE_DIR`: private per-plugin/per-job build workspace, default `<PLATFORM_DATA_DIR>/distribution-builds`.
|
||||||
- `PLATFORM_BUILDER_CACHE_DIR`: persistent Go build/module cache, default `<PLATFORM_DATA_DIR>/distribution-build-cache`; it contains no job inputs or component keys.
|
- `PLATFORM_BUILDER_CACHE_DIR`: persistent Go build/module cache, default `<PLATFORM_DATA_DIR>/distribution-build-cache`; it contains no job inputs or component keys.
|
||||||
- `PLATFORM_BUILDER_TIMEOUT_SECONDS`: positive build deadline, default `1800`.
|
- `PLATFORM_BUILDER_TIMEOUT_SECONDS`: positive build deadline, default `1800`.
|
||||||
- `PLATFORM_RUN_RELEASE_URL`: public platform URL embedded into generated components, default `https://scum.npc0.com/`.
|
- `PLATFORM_RUN_RELEASE_URL`: public platform URL embedded into generated Run packages, default `http://127.0.0.1:8080/`; set this explicitly for tunnel or production access.
|
||||||
|
|
||||||
Build the dedicated toolchain image before enabling distribution generation:
|
Build the dedicated toolchain image before enabling distribution generation:
|
||||||
|
|
||||||
|
|||||||
@@ -912,6 +912,10 @@ func (h *coreHandlers) gamePlugins(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeDecodeError(w, err)
|
writeDecodeError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if violations := request.RuntimeProfiles.UnsupportedLegacyProfileViolations("runtimeProfiles"); len(violations) > 0 {
|
||||||
|
writeServiceError(w, validator.ValidationError{Violations: violations})
|
||||||
|
return
|
||||||
|
}
|
||||||
plugin, err := h.core.CreateGamePlugin(request.ToDomain())
|
plugin, err := h.core.CreateGamePlugin(request.ToDomain())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeServiceError(w, err)
|
writeServiceError(w, err)
|
||||||
@@ -944,7 +948,7 @@ func (h *coreHandlers) gamePluginManifestRegistration(w http.ResponseWriter, r *
|
|||||||
writeDecodeError(w, err)
|
writeDecodeError(w, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if violations := request.Manifest.RuntimeProfiles.UnsupportedLegacyClientManagerViolations("manifest.runtimeProfiles"); len(violations) > 0 {
|
if violations := request.Manifest.RuntimeProfiles.UnsupportedLegacyProfileViolations("manifest.runtimeProfiles"); len(violations) > 0 {
|
||||||
writeServiceError(w, validator.ValidationError{Violations: violations})
|
writeServiceError(w, validator.ValidationError{Violations: violations})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,6 +129,24 @@ func TestCoreAPICreateListDetailWorkflows(t *testing.T) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCoreAPIGamePluginCreateRejectsLegacyServerDeployments(t *testing.T) {
|
||||||
|
router := newTestRouter()
|
||||||
|
request := validGamePluginRequest()
|
||||||
|
request.RuntimeProfiles.ServerDeployments = json.RawMessage(`[{"key":"legacy"}]`)
|
||||||
|
|
||||||
|
response := performJSON(t, router, http.MethodPost, "/api/v1/game-plugins", request)
|
||||||
|
assertErrorResponse(t, response, http.StatusBadRequest, errorCodeValidation)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCoreAPIGamePluginManifestRejectsLegacyServerDeployments(t *testing.T) {
|
||||||
|
router := newTestRouter()
|
||||||
|
registration := validGamePluginManifestRegistrationRequest()
|
||||||
|
registration.Manifest.RuntimeProfiles.ServerDeployments = json.RawMessage(`[{"key":"legacy"}]`)
|
||||||
|
|
||||||
|
response := performJSON(t, router, http.MethodPost, "/api/v1/game-plugins/register-manifest", registration)
|
||||||
|
assertErrorResponse(t, response, http.StatusBadRequest, errorCodeValidation)
|
||||||
|
}
|
||||||
|
|
||||||
func TestMetricsAndConfigReadAPIAreSafeAndRoleScoped(t *testing.T) {
|
func TestMetricsAndConfigReadAPIAreSafeAndRoleScoped(t *testing.T) {
|
||||||
router := newTestRouter()
|
router := newTestRouter()
|
||||||
adminSession := createAdminSession(t, router)
|
adminSession := createAdminSession(t, router)
|
||||||
@@ -1519,7 +1537,7 @@ func TestGamePluginManifestAPISafelyProjectsDLLReleaseDeclaration(t *testing.T)
|
|||||||
Key: "scum-simple-rcon", DisplayName: "SCUM Simple RCON", Kind: "ue4ss-dll", Activation: "server-start", Version: "0.1.0", ReleaseState: "ready",
|
Key: "scum-simple-rcon", DisplayName: "SCUM Simple RCON", Kind: "ue4ss-dll", Activation: "server-start", Version: "0.1.0", ReleaseState: "ready",
|
||||||
ReleaseURL: "https://cdn.npc0.com/scum_simple_rcon_ue4s.dll", Checksum: "sha256:" + strings.Repeat("a", 64), SizeBytes: 1024,
|
ReleaseURL: "https://cdn.npc0.com/scum_simple_rcon_ue4s.dll", Checksum: "sha256:" + strings.Repeat("a", 64), SizeBytes: 1024,
|
||||||
TargetKey: "ue4ss/scum-simple-rcon", ModKey: "scum_simple_rcon", DLLRef: "ue4ss/Mods/scum_simple_rcon/dlls/main.dll",
|
TargetKey: "ue4ss/scum-simple-rcon", ModKey: "scum_simple_rcon", DLLRef: "ue4ss/Mods/scum_simple_rcon/dlls/main.dll",
|
||||||
SCUMExecutableChecksum: "sha256:" + strings.Repeat("b", 64), UE4SSABI: "ue4ss-3.0", SupportedTargets: []dto.RuntimeTargetBody{{OS: "windows", Arch: "amd64"}}, UpdateOnStart: true, RCONPort: 27015,
|
TargetExecutableChecksum: "sha256:" + strings.Repeat("b", 64), UE4SSABI: "ue4ss-3.0", SupportedTargets: []dto.RuntimeTargetBody{{OS: "windows", Arch: "amd64"}}, UpdateOnStart: true, RCONPort: 27015,
|
||||||
}}}
|
}}}
|
||||||
|
|
||||||
recorder := performJSON(t, router, http.MethodPost, "/api/v1/game-plugins/register-manifest", registration)
|
recorder := performJSON(t, router, http.MethodPost, "/api/v1/game-plugins/register-manifest", registration)
|
||||||
|
|||||||
@@ -197,17 +197,17 @@ type RunAutonomousLogSource struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RunAutonomousDLLExtension struct {
|
type RunAutonomousDLLExtension struct {
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
ReleaseURL string `json:"releaseUrl"`
|
ReleaseURL string `json:"releaseUrl"`
|
||||||
Checksum string `json:"checksum"`
|
Checksum string `json:"checksum"`
|
||||||
SizeBytes int64 `json:"sizeBytes,omitempty"`
|
SizeBytes int64 `json:"sizeBytes,omitempty"`
|
||||||
TargetKey string `json:"targetKey"`
|
TargetKey string `json:"targetKey"`
|
||||||
ModKey string `json:"modKey"`
|
ModKey string `json:"modKey"`
|
||||||
DLLRef string `json:"dllRef"`
|
DLLRef string `json:"dllRef"`
|
||||||
SCUMExecutableChecksum string `json:"scumExecutableChecksum,omitempty"`
|
TargetExecutableChecksum string `json:"targetExecutableChecksum,omitempty"`
|
||||||
UE4SSABI string `json:"ue4ssAbi,omitempty"`
|
UE4SSABI string `json:"ue4ssAbi,omitempty"`
|
||||||
RCONPort int `json:"rconPort,omitempty"`
|
RCONPort int `json:"rconPort,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RunAutonomousDataTarget is a package-local snapshot declaration. The source
|
// RunAutonomousDataTarget is a package-local snapshot declaration. The source
|
||||||
|
|||||||
@@ -462,41 +462,41 @@ type RuntimeDataTarget struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RuntimeDLLExtensionProfile struct {
|
type RuntimeDLLExtensionProfile struct {
|
||||||
Key string
|
Key string
|
||||||
DisplayName string
|
DisplayName string
|
||||||
Kind string
|
Kind string
|
||||||
Activation string
|
Activation string
|
||||||
Version string
|
Version string
|
||||||
ReleaseState string
|
ReleaseState string
|
||||||
ReleaseURL string
|
ReleaseURL string
|
||||||
Checksum string
|
Checksum string
|
||||||
SizeBytes int64
|
SizeBytes int64
|
||||||
TargetKey string
|
TargetKey string
|
||||||
ModKey string
|
ModKey string
|
||||||
DLLRef string
|
DLLRef string
|
||||||
SCUMExecutableChecksum string
|
TargetExecutableChecksum string
|
||||||
UE4SSABI string
|
UE4SSABI string
|
||||||
SupportedTargets []RuntimeTarget
|
SupportedTargets []RuntimeTarget
|
||||||
UpdateOnStart bool
|
UpdateOnStart bool
|
||||||
RCONPort int
|
RCONPort int
|
||||||
}
|
}
|
||||||
|
|
||||||
type RuntimeDLLExtensionPlan struct {
|
type RuntimeDLLExtensionPlan struct {
|
||||||
Key string
|
Key string
|
||||||
Version string
|
Version string
|
||||||
ReleaseURL string
|
ReleaseURL string
|
||||||
Checksum string
|
Checksum string
|
||||||
SizeBytes int64
|
SizeBytes int64
|
||||||
TargetKey string
|
TargetKey string
|
||||||
ModKey string
|
ModKey string
|
||||||
DLLRef string
|
DLLRef string
|
||||||
SCUMExecutableChecksum string
|
TargetExecutableChecksum string
|
||||||
UE4SSABI string
|
UE4SSABI string
|
||||||
RCONPort int
|
RCONPort int
|
||||||
}
|
}
|
||||||
|
|
||||||
// RuntimeSourceRCONPlan is a frozen, secret-free loopback connection plan for
|
// RuntimeSourceRCONPlan is a frozen, secret-free loopback connection plan for
|
||||||
// a ready SCUM UE4SS extension. The generated local config remains Run-owned.
|
// a ready UE4SS extension. The generated local config remains Run-owned.
|
||||||
type RuntimeSourceRCONPlan struct {
|
type RuntimeSourceRCONPlan struct {
|
||||||
Protocol string
|
Protocol string
|
||||||
ExtensionKey string
|
ExtensionKey string
|
||||||
@@ -543,29 +543,11 @@ type RuntimeServerPrerequisite struct {
|
|||||||
Kind string
|
Kind string
|
||||||
}
|
}
|
||||||
|
|
||||||
// RuntimeServerDeploymentProfile is a legacy game-specific deployment template
|
|
||||||
// declaration kept for backward-compatible manifest decoding.
|
|
||||||
type RuntimeServerDeploymentProfile struct {
|
|
||||||
Key string
|
|
||||||
Version string
|
|
||||||
SupportedTargets []RuntimeTarget
|
|
||||||
SteamAppID string
|
|
||||||
ExecutableKey string
|
|
||||||
InstallRootKey string
|
|
||||||
ConfigKey string
|
|
||||||
ConfigFormat string
|
|
||||||
Prerequisites []RuntimeServerPrerequisite
|
|
||||||
ConfigMappings []RuntimeServerConfigMapping
|
|
||||||
DiscoveryMarkers []RuntimeServerDiscoveryMarker
|
|
||||||
VerificationChecks []RuntimeServerVerificationCheck
|
|
||||||
}
|
|
||||||
|
|
||||||
type GamePluginRuntimeProfiles struct {
|
type GamePluginRuntimeProfiles struct {
|
||||||
Discovery []RuntimeDiscoveryProbe
|
Discovery []RuntimeDiscoveryProbe
|
||||||
LifecycleProfiles []RuntimeLifecycleProfile
|
LifecycleProfiles []RuntimeLifecycleProfile
|
||||||
DependencyProbes []RuntimeDependencyProbe
|
DependencyProbes []RuntimeDependencyProbe
|
||||||
InstallPlans []RuntimeInstallPlan
|
InstallPlans []RuntimeInstallPlan
|
||||||
ServerDeployments []RuntimeServerDeploymentProfile
|
|
||||||
LogSources []RuntimeLogSource
|
LogSources []RuntimeLogSource
|
||||||
TransportProfiles []RuntimeTransportProfile
|
TransportProfiles []RuntimeTransportProfile
|
||||||
DataTargets []RuntimeDataTarget
|
DataTargets []RuntimeDataTarget
|
||||||
@@ -1826,10 +1808,6 @@ func CopyGamePluginRuntimeProfiles(profiles GamePluginRuntimeProfiles) GamePlugi
|
|||||||
profiles.InstallPlans[i].Platforms = CopyStringSlice(profiles.InstallPlans[i].Platforms)
|
profiles.InstallPlans[i].Platforms = CopyStringSlice(profiles.InstallPlans[i].Platforms)
|
||||||
profiles.InstallPlans[i].Steps = append([]RuntimeInstallStep(nil), profiles.InstallPlans[i].Steps...)
|
profiles.InstallPlans[i].Steps = append([]RuntimeInstallStep(nil), profiles.InstallPlans[i].Steps...)
|
||||||
}
|
}
|
||||||
profiles.ServerDeployments = append([]RuntimeServerDeploymentProfile(nil), profiles.ServerDeployments...)
|
|
||||||
for i := range profiles.ServerDeployments {
|
|
||||||
profiles.ServerDeployments[i] = CopyRuntimeServerDeploymentProfile(profiles.ServerDeployments[i])
|
|
||||||
}
|
|
||||||
profiles.LogSources = append([]RuntimeLogSource(nil), profiles.LogSources...)
|
profiles.LogSources = append([]RuntimeLogSource(nil), profiles.LogSources...)
|
||||||
profiles.TransportProfiles = append([]RuntimeTransportProfile(nil), profiles.TransportProfiles...)
|
profiles.TransportProfiles = append([]RuntimeTransportProfile(nil), profiles.TransportProfiles...)
|
||||||
for i := range profiles.TransportProfiles {
|
for i := range profiles.TransportProfiles {
|
||||||
@@ -1895,15 +1873,6 @@ func CopyServerInstance(instance ServerInstance) ServerInstance {
|
|||||||
return instance
|
return instance
|
||||||
}
|
}
|
||||||
|
|
||||||
func CopyRuntimeServerDeploymentProfile(profile RuntimeServerDeploymentProfile) RuntimeServerDeploymentProfile {
|
|
||||||
profile.Prerequisites = append([]RuntimeServerPrerequisite(nil), profile.Prerequisites...)
|
|
||||||
profile.SupportedTargets = append([]RuntimeTarget(nil), profile.SupportedTargets...)
|
|
||||||
profile.ConfigMappings = append([]RuntimeServerConfigMapping(nil), profile.ConfigMappings...)
|
|
||||||
profile.DiscoveryMarkers = append([]RuntimeServerDiscoveryMarker(nil), profile.DiscoveryMarkers...)
|
|
||||||
profile.VerificationChecks = append([]RuntimeServerVerificationCheck(nil), profile.VerificationChecks...)
|
|
||||||
return profile
|
|
||||||
}
|
|
||||||
|
|
||||||
func CopyServerDeploymentProjection(projection ServerDeploymentProjection) ServerDeploymentProjection {
|
func CopyServerDeploymentProjection(projection ServerDeploymentProjection) ServerDeploymentProjection {
|
||||||
projection.DiscoveredFacts = CopyStringMap(projection.DiscoveredFacts)
|
projection.DiscoveredFacts = CopyStringMap(projection.DiscoveredFacts)
|
||||||
projection.MappingResults = CopyStringMap(projection.MappingResults)
|
projection.MappingResults = CopyStringMap(projection.MappingResults)
|
||||||
|
|||||||
@@ -82,21 +82,6 @@ type RuntimeServerVerificationCheckBody struct {
|
|||||||
Required bool `json:"required,omitempty"`
|
Required bool `json:"required,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RuntimeServerDeploymentProfileBody struct {
|
|
||||||
Key string `json:"key"`
|
|
||||||
Version string `json:"version"`
|
|
||||||
SupportedTargets []RuntimeTargetBody `json:"supportedTargets"`
|
|
||||||
SteamAppID string `json:"steamAppId"`
|
|
||||||
ExecutableKey string `json:"executableKey"`
|
|
||||||
InstallRootKey string `json:"installRootKey"`
|
|
||||||
ConfigKey string `json:"configKey"`
|
|
||||||
ConfigFormat string `json:"configFormat"`
|
|
||||||
Prerequisites []RuntimeServerPrerequisiteBody `json:"prerequisites,omitempty"`
|
|
||||||
ConfigMappings []RuntimeServerConfigMappingBody `json:"configMappings"`
|
|
||||||
DiscoveryMarkers []RuntimeServerDiscoveryMarkerBody `json:"discoveryMarkers"`
|
|
||||||
VerificationChecks []RuntimeServerVerificationCheckBody `json:"verificationChecks"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RuntimeServerPrerequisiteBody struct {
|
type RuntimeServerPrerequisiteBody struct {
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
Kind string `json:"kind"`
|
Kind string `json:"kind"`
|
||||||
@@ -151,77 +136,77 @@ type RuntimeConfigTemplateBody struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RuntimeDLLExtensionProfileBody struct {
|
type RuntimeDLLExtensionProfileBody struct {
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
DisplayName string `json:"displayName"`
|
DisplayName string `json:"displayName"`
|
||||||
Kind string `json:"kind"`
|
Kind string `json:"kind"`
|
||||||
Activation string `json:"activation"`
|
Activation string `json:"activation"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
ReleaseState string `json:"releaseState"`
|
ReleaseState string `json:"releaseState"`
|
||||||
ReleaseURL string `json:"releaseUrl,omitempty"`
|
ReleaseURL string `json:"releaseUrl,omitempty"`
|
||||||
ReleaseHost string `json:"releaseHost,omitempty"`
|
ReleaseHost string `json:"releaseHost,omitempty"`
|
||||||
ReleaseFilename string `json:"releaseFilename,omitempty"`
|
ReleaseFilename string `json:"releaseFilename,omitempty"`
|
||||||
Checksum string `json:"checksum,omitempty"`
|
Checksum string `json:"checksum,omitempty"`
|
||||||
SizeBytes int64 `json:"sizeBytes,omitempty"`
|
SizeBytes int64 `json:"sizeBytes,omitempty"`
|
||||||
TargetKey string `json:"targetKey"`
|
TargetKey string `json:"targetKey"`
|
||||||
ModKey string `json:"modKey"`
|
ModKey string `json:"modKey"`
|
||||||
DLLRef string `json:"dllRef"`
|
DLLRef string `json:"dllRef"`
|
||||||
SCUMExecutableChecksum string `json:"scumExecutableChecksum,omitempty"`
|
TargetExecutableChecksum string `json:"targetExecutableChecksum,omitempty"`
|
||||||
UE4SSABI string `json:"ue4ssAbi,omitempty"`
|
UE4SSABI string `json:"ue4ssAbi,omitempty"`
|
||||||
SupportedTargets []RuntimeTargetBody `json:"supportedTargets"`
|
SupportedTargets []RuntimeTargetBody `json:"supportedTargets"`
|
||||||
UpdateOnStart bool `json:"updateOnStart"`
|
UpdateOnStart bool `json:"updateOnStart"`
|
||||||
RCONPort int `json:"rconPort"`
|
RCONPort int `json:"rconPort"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RuntimeDLLExtensionProfileResponseBody is the browser-safe projection of a
|
// RuntimeDLLExtensionProfileResponseBody is the browser-safe projection of a
|
||||||
// declared DLL extension. The immutable deployment path, RCON port, and full
|
// declared DLL extension. The immutable deployment path, RCON port, and full
|
||||||
// release URL remain internal to the manifest/start-job contracts.
|
// release URL remain internal to the manifest/start-job contracts.
|
||||||
type RuntimeDLLExtensionProfileResponseBody struct {
|
type RuntimeDLLExtensionProfileResponseBody struct {
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
DisplayName string `json:"displayName"`
|
DisplayName string `json:"displayName"`
|
||||||
Kind string `json:"kind"`
|
Kind string `json:"kind"`
|
||||||
Activation string `json:"activation"`
|
Activation string `json:"activation"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
ReleaseState string `json:"releaseState"`
|
ReleaseState string `json:"releaseState"`
|
||||||
ReleaseHost string `json:"releaseHost,omitempty"`
|
ReleaseHost string `json:"releaseHost,omitempty"`
|
||||||
ReleaseFilename string `json:"releaseFilename,omitempty"`
|
ReleaseFilename string `json:"releaseFilename,omitempty"`
|
||||||
Checksum string `json:"checksum,omitempty"`
|
Checksum string `json:"checksum,omitempty"`
|
||||||
SizeBytes int64 `json:"sizeBytes,omitempty"`
|
SizeBytes int64 `json:"sizeBytes,omitempty"`
|
||||||
SCUMExecutableChecksum string `json:"scumExecutableChecksum,omitempty"`
|
TargetExecutableChecksum string `json:"targetExecutableChecksum,omitempty"`
|
||||||
UE4SSABI string `json:"ue4ssAbi,omitempty"`
|
UE4SSABI string `json:"ue4ssAbi,omitempty"`
|
||||||
SupportedTargets []RuntimeTargetBody `json:"supportedTargets"`
|
SupportedTargets []RuntimeTargetBody `json:"supportedTargets"`
|
||||||
UpdateOnStart bool `json:"updateOnStart"`
|
UpdateOnStart bool `json:"updateOnStart"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RuntimeDLLExtensionPlanBody struct {
|
type RuntimeDLLExtensionPlanBody struct {
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
ReleaseURL string `json:"releaseUrl"`
|
ReleaseURL string `json:"releaseUrl"`
|
||||||
Checksum string `json:"checksum"`
|
Checksum string `json:"checksum"`
|
||||||
SizeBytes int64 `json:"sizeBytes"`
|
SizeBytes int64 `json:"sizeBytes"`
|
||||||
TargetKey string `json:"targetKey"`
|
TargetKey string `json:"targetKey"`
|
||||||
ModKey string `json:"modKey"`
|
ModKey string `json:"modKey"`
|
||||||
DLLRef string `json:"dllRef"`
|
DLLRef string `json:"dllRef"`
|
||||||
SCUMExecutableChecksum string `json:"scumExecutableChecksum"`
|
TargetExecutableChecksum string `json:"targetExecutableChecksum"`
|
||||||
UE4SSABI string `json:"ue4ssAbi"`
|
UE4SSABI string `json:"ue4ssAbi"`
|
||||||
RCONPort int `json:"rconPort"`
|
RCONPort int `json:"rconPort"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GamePluginRuntimeProfilesBody struct {
|
type GamePluginRuntimeProfilesBody struct {
|
||||||
Discovery []RuntimeDiscoveryProbeBody `json:"discovery,omitempty"`
|
Discovery []RuntimeDiscoveryProbeBody `json:"discovery,omitempty"`
|
||||||
LifecycleProfiles []RuntimeLifecycleProfileBody `json:"lifecycleProfiles,omitempty"`
|
LifecycleProfiles []RuntimeLifecycleProfileBody `json:"lifecycleProfiles,omitempty"`
|
||||||
DependencyProbes []RuntimeDependencyProbeBody `json:"dependencyProbes,omitempty"`
|
DependencyProbes []RuntimeDependencyProbeBody `json:"dependencyProbes,omitempty"`
|
||||||
InstallPlans []RuntimeInstallPlanBody `json:"installPlans,omitempty"`
|
InstallPlans []RuntimeInstallPlanBody `json:"installPlans,omitempty"`
|
||||||
ServerDeployments []RuntimeServerDeploymentProfileBody `json:"serverDeployments,omitempty"`
|
ServerDeployments json.RawMessage `json:"serverDeployments,omitempty"`
|
||||||
LogSources []RuntimeLogSourceBody `json:"logSources,omitempty"`
|
LogSources []RuntimeLogSourceBody `json:"logSources,omitempty"`
|
||||||
TransportProfiles []RuntimeTransportProfileBody `json:"transportProfiles,omitempty"`
|
TransportProfiles []RuntimeTransportProfileBody `json:"transportProfiles,omitempty"`
|
||||||
DataTargets []RuntimeDataTargetBody `json:"dataTargets,omitempty"`
|
DataTargets []RuntimeDataTargetBody `json:"dataTargets,omitempty"`
|
||||||
ClientManagers *[]json.RawMessage `json:"clientManagers,omitempty"`
|
ClientManagers json.RawMessage `json:"clientManagers,omitempty"`
|
||||||
DLLExtensions []RuntimeDLLExtensionProfileBody `json:"dllExtensions,omitempty"`
|
DLLExtensions []RuntimeDLLExtensionProfileBody `json:"dllExtensions,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (body GamePluginRuntimeProfilesBody) UnsupportedLegacyClientManagerViolations(prefix string) []string {
|
func (body GamePluginRuntimeProfilesBody) UnsupportedLegacyProfileViolations(prefix string) []string {
|
||||||
var violations []string
|
var violations []string
|
||||||
if body.ClientManagers != nil {
|
if len(body.ClientManagers) > 0 {
|
||||||
violations = append(violations, prefix+".clientManagers is no longer supported")
|
violations = append(violations, prefix+".clientManagers is no longer supported")
|
||||||
}
|
}
|
||||||
for i, profile := range body.LifecycleProfiles {
|
for i, profile := range body.LifecycleProfiles {
|
||||||
@@ -229,6 +214,9 @@ func (body GamePluginRuntimeProfilesBody) UnsupportedLegacyClientManagerViolatio
|
|||||||
violations = append(violations, fmt.Sprintf("%s.lifecycleProfiles[%d].clientManagerRef is no longer supported", prefix, i))
|
violations = append(violations, fmt.Sprintf("%s.lifecycleProfiles[%d].clientManagerRef is no longer supported", prefix, i))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(body.ServerDeployments) > 0 {
|
||||||
|
violations = append(violations, prefix+".serverDeployments is no longer supported")
|
||||||
|
}
|
||||||
return violations
|
return violations
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,7 +228,6 @@ type GamePluginRuntimeProfilesResponseBody struct {
|
|||||||
LifecycleProfiles []RuntimeLifecycleProfileBody `json:"lifecycleProfiles,omitempty"`
|
LifecycleProfiles []RuntimeLifecycleProfileBody `json:"lifecycleProfiles,omitempty"`
|
||||||
DependencyProbes []RuntimeDependencyProbeBody `json:"dependencyProbes,omitempty"`
|
DependencyProbes []RuntimeDependencyProbeBody `json:"dependencyProbes,omitempty"`
|
||||||
InstallPlans []RuntimeInstallPlanBody `json:"installPlans,omitempty"`
|
InstallPlans []RuntimeInstallPlanBody `json:"installPlans,omitempty"`
|
||||||
ServerDeployments []RuntimeServerDeploymentProfileBody `json:"serverDeployments,omitempty"`
|
|
||||||
LogSources []RuntimeLogSourceBody `json:"logSources,omitempty"`
|
LogSources []RuntimeLogSourceBody `json:"logSources,omitempty"`
|
||||||
TransportProfiles []RuntimeTransportProfileBody `json:"transportProfiles,omitempty"`
|
TransportProfiles []RuntimeTransportProfileBody `json:"transportProfiles,omitempty"`
|
||||||
DataTargets []RuntimeDataTargetBody `json:"dataTargets,omitempty"`
|
DataTargets []RuntimeDataTargetBody `json:"dataTargets,omitempty"`
|
||||||
@@ -265,25 +252,6 @@ func (body GamePluginRuntimeProfilesBody) ToDomain() domain.GamePluginRuntimePro
|
|||||||
}
|
}
|
||||||
profiles.InstallPlans = append(profiles.InstallPlans, plan)
|
profiles.InstallPlans = append(profiles.InstallPlans, plan)
|
||||||
}
|
}
|
||||||
for _, item := range body.ServerDeployments {
|
|
||||||
profile := domain.RuntimeServerDeploymentProfile{Key: item.Key, Version: item.Version, SteamAppID: item.SteamAppID, ExecutableKey: item.ExecutableKey, InstallRootKey: item.InstallRootKey, ConfigKey: item.ConfigKey, ConfigFormat: item.ConfigFormat}
|
|
||||||
for _, prerequisite := range item.Prerequisites {
|
|
||||||
profile.Prerequisites = append(profile.Prerequisites, domain.RuntimeServerPrerequisite{Key: prerequisite.Key, Kind: prerequisite.Kind})
|
|
||||||
}
|
|
||||||
for _, target := range item.SupportedTargets {
|
|
||||||
profile.SupportedTargets = append(profile.SupportedTargets, domain.RuntimeTarget{OS: target.OS, Arch: target.Arch})
|
|
||||||
}
|
|
||||||
for _, mapping := range item.ConfigMappings {
|
|
||||||
profile.ConfigMappings = append(profile.ConfigMappings, domain.RuntimeServerConfigMapping{FieldKey: mapping.FieldKey, ConfigKey: mapping.ConfigKey, ValueType: mapping.ValueType, Required: mapping.Required})
|
|
||||||
}
|
|
||||||
for _, marker := range item.DiscoveryMarkers {
|
|
||||||
profile.DiscoveryMarkers = append(profile.DiscoveryMarkers, domain.RuntimeServerDiscoveryMarker{Key: marker.Key, Kind: marker.Kind, TargetKey: marker.TargetKey, Expected: marker.Expected, Required: marker.Required})
|
|
||||||
}
|
|
||||||
for _, check := range item.VerificationChecks {
|
|
||||||
profile.VerificationChecks = append(profile.VerificationChecks, domain.RuntimeServerVerificationCheck{Key: check.Key, Kind: check.Kind, TargetKey: check.TargetKey, Required: check.Required})
|
|
||||||
}
|
|
||||||
profiles.ServerDeployments = append(profiles.ServerDeployments, profile)
|
|
||||||
}
|
|
||||||
for _, item := range body.LogSources {
|
for _, item := range body.LogSources {
|
||||||
profiles.LogSources = append(profiles.LogSources, domain.RuntimeLogSource{Key: item.Key, Kind: item.Kind, TargetKey: item.TargetKey, StreamKey: item.StreamKey, CursorKind: item.CursorKind, RetentionDays: item.RetentionDays})
|
profiles.LogSources = append(profiles.LogSources, domain.RuntimeLogSource{Key: item.Key, Kind: item.Kind, TargetKey: item.TargetKey, StreamKey: item.StreamKey, CursorKind: item.CursorKind, RetentionDays: item.RetentionDays})
|
||||||
}
|
}
|
||||||
@@ -294,7 +262,7 @@ func (body GamePluginRuntimeProfilesBody) ToDomain() domain.GamePluginRuntimePro
|
|||||||
profiles.DataTargets = append(profiles.DataTargets, domain.RuntimeDataTarget{Key: item.Key, Kind: item.Kind, TransportKey: item.TransportKey, SourceRootKey: item.SourceRootKey, SourcePath: item.SourcePath, WorkspaceKey: item.WorkspaceKey, RefreshPolicy: item.RefreshPolicy, MaxBytes: item.MaxBytes, Platforms: domain.CopyStringSlice(item.Platforms)})
|
profiles.DataTargets = append(profiles.DataTargets, domain.RuntimeDataTarget{Key: item.Key, Kind: item.Kind, TransportKey: item.TransportKey, SourceRootKey: item.SourceRootKey, SourcePath: item.SourcePath, WorkspaceKey: item.WorkspaceKey, RefreshPolicy: item.RefreshPolicy, MaxBytes: item.MaxBytes, Platforms: domain.CopyStringSlice(item.Platforms)})
|
||||||
}
|
}
|
||||||
for _, item := range body.DLLExtensions {
|
for _, item := range body.DLLExtensions {
|
||||||
extension := domain.RuntimeDLLExtensionProfile{Key: item.Key, DisplayName: item.DisplayName, Kind: item.Kind, Activation: item.Activation, Version: item.Version, ReleaseState: item.ReleaseState, ReleaseURL: item.ReleaseURL, Checksum: item.Checksum, SizeBytes: item.SizeBytes, TargetKey: item.TargetKey, ModKey: item.ModKey, DLLRef: item.DLLRef, SCUMExecutableChecksum: item.SCUMExecutableChecksum, UE4SSABI: item.UE4SSABI, UpdateOnStart: item.UpdateOnStart, RCONPort: item.RCONPort}
|
extension := domain.RuntimeDLLExtensionProfile{Key: item.Key, DisplayName: item.DisplayName, Kind: item.Kind, Activation: item.Activation, Version: item.Version, ReleaseState: item.ReleaseState, ReleaseURL: item.ReleaseURL, Checksum: item.Checksum, SizeBytes: item.SizeBytes, TargetKey: item.TargetKey, ModKey: item.ModKey, DLLRef: item.DLLRef, TargetExecutableChecksum: item.TargetExecutableChecksum, UE4SSABI: item.UE4SSABI, UpdateOnStart: item.UpdateOnStart, RCONPort: item.RCONPort}
|
||||||
for _, target := range item.SupportedTargets {
|
for _, target := range item.SupportedTargets {
|
||||||
extension.SupportedTargets = append(extension.SupportedTargets, domain.RuntimeTarget{OS: target.OS, Arch: target.Arch})
|
extension.SupportedTargets = append(extension.SupportedTargets, domain.RuntimeTarget{OS: target.OS, Arch: target.Arch})
|
||||||
}
|
}
|
||||||
@@ -322,25 +290,6 @@ func runtimeProfilesFromDomain(profiles domain.GamePluginRuntimeProfiles) GamePl
|
|||||||
}
|
}
|
||||||
body.InstallPlans = append(body.InstallPlans, plan)
|
body.InstallPlans = append(body.InstallPlans, plan)
|
||||||
}
|
}
|
||||||
for _, item := range profiles.ServerDeployments {
|
|
||||||
bodyProfile := RuntimeServerDeploymentProfileBody{Key: item.Key, Version: item.Version, SteamAppID: item.SteamAppID, ExecutableKey: item.ExecutableKey, InstallRootKey: item.InstallRootKey, ConfigKey: item.ConfigKey, ConfigFormat: item.ConfigFormat}
|
|
||||||
for _, prerequisite := range item.Prerequisites {
|
|
||||||
bodyProfile.Prerequisites = append(bodyProfile.Prerequisites, RuntimeServerPrerequisiteBody{Key: prerequisite.Key, Kind: prerequisite.Kind})
|
|
||||||
}
|
|
||||||
for _, target := range item.SupportedTargets {
|
|
||||||
bodyProfile.SupportedTargets = append(bodyProfile.SupportedTargets, RuntimeTargetBody{OS: target.OS, Arch: target.Arch})
|
|
||||||
}
|
|
||||||
for _, mapping := range item.ConfigMappings {
|
|
||||||
bodyProfile.ConfigMappings = append(bodyProfile.ConfigMappings, RuntimeServerConfigMappingBody{FieldKey: mapping.FieldKey, ConfigKey: mapping.ConfigKey, ValueType: mapping.ValueType, Required: mapping.Required})
|
|
||||||
}
|
|
||||||
for _, marker := range item.DiscoveryMarkers {
|
|
||||||
bodyProfile.DiscoveryMarkers = append(bodyProfile.DiscoveryMarkers, RuntimeServerDiscoveryMarkerBody{Key: marker.Key, Kind: marker.Kind, TargetKey: marker.TargetKey, Expected: marker.Expected, Required: marker.Required})
|
|
||||||
}
|
|
||||||
for _, check := range item.VerificationChecks {
|
|
||||||
bodyProfile.VerificationChecks = append(bodyProfile.VerificationChecks, RuntimeServerVerificationCheckBody{Key: check.Key, Kind: check.Kind, TargetKey: check.TargetKey, Required: check.Required})
|
|
||||||
}
|
|
||||||
body.ServerDeployments = append(body.ServerDeployments, bodyProfile)
|
|
||||||
}
|
|
||||||
for _, item := range profiles.LogSources {
|
for _, item := range profiles.LogSources {
|
||||||
body.LogSources = append(body.LogSources, RuntimeLogSourceBody{Key: item.Key, Kind: item.Kind, TargetKey: item.TargetKey, StreamKey: item.StreamKey, CursorKind: item.CursorKind, RetentionDays: item.RetentionDays})
|
body.LogSources = append(body.LogSources, RuntimeLogSourceBody{Key: item.Key, Kind: item.Kind, TargetKey: item.TargetKey, StreamKey: item.StreamKey, CursorKind: item.CursorKind, RetentionDays: item.RetentionDays})
|
||||||
}
|
}
|
||||||
@@ -352,7 +301,7 @@ func runtimeProfilesFromDomain(profiles domain.GamePluginRuntimeProfiles) GamePl
|
|||||||
}
|
}
|
||||||
for _, item := range profiles.DLLExtensions {
|
for _, item := range profiles.DLLExtensions {
|
||||||
host, filename := safeDLLReleaseLocation(item.ReleaseURL)
|
host, filename := safeDLLReleaseLocation(item.ReleaseURL)
|
||||||
extension := RuntimeDLLExtensionProfileResponseBody{Key: item.Key, DisplayName: item.DisplayName, Kind: item.Kind, Activation: item.Activation, Version: item.Version, ReleaseState: item.ReleaseState, ReleaseHost: host, ReleaseFilename: filename, Checksum: safeDLLChecksumPrefix(item.Checksum), SizeBytes: item.SizeBytes, SCUMExecutableChecksum: safeDLLChecksumPrefix(item.SCUMExecutableChecksum), UE4SSABI: item.UE4SSABI, UpdateOnStart: item.UpdateOnStart}
|
extension := RuntimeDLLExtensionProfileResponseBody{Key: item.Key, DisplayName: item.DisplayName, Kind: item.Kind, Activation: item.Activation, Version: item.Version, ReleaseState: item.ReleaseState, ReleaseHost: host, ReleaseFilename: filename, Checksum: safeDLLChecksumPrefix(item.Checksum), SizeBytes: item.SizeBytes, TargetExecutableChecksum: safeDLLChecksumPrefix(item.TargetExecutableChecksum), UE4SSABI: item.UE4SSABI, UpdateOnStart: item.UpdateOnStart}
|
||||||
for _, target := range item.SupportedTargets {
|
for _, target := range item.SupportedTargets {
|
||||||
extension.SupportedTargets = append(extension.SupportedTargets, RuntimeTargetBody{OS: target.OS, Arch: target.Arch})
|
extension.SupportedTargets = append(extension.SupportedTargets, RuntimeTargetBody{OS: target.OS, Arch: target.Arch})
|
||||||
}
|
}
|
||||||
@@ -364,7 +313,7 @@ func runtimeProfilesFromDomain(profiles domain.GamePluginRuntimeProfiles) GamePl
|
|||||||
func dllExtensionPlansFromDomain(plans []domain.RuntimeDLLExtensionPlan) []RuntimeDLLExtensionPlanBody {
|
func dllExtensionPlansFromDomain(plans []domain.RuntimeDLLExtensionPlan) []RuntimeDLLExtensionPlanBody {
|
||||||
items := make([]RuntimeDLLExtensionPlanBody, 0, len(plans))
|
items := make([]RuntimeDLLExtensionPlanBody, 0, len(plans))
|
||||||
for _, item := range plans {
|
for _, item := range plans {
|
||||||
items = append(items, RuntimeDLLExtensionPlanBody{Key: item.Key, Version: item.Version, ReleaseURL: item.ReleaseURL, Checksum: item.Checksum, SizeBytes: item.SizeBytes, TargetKey: item.TargetKey, ModKey: item.ModKey, DLLRef: item.DLLRef, SCUMExecutableChecksum: item.SCUMExecutableChecksum, UE4SSABI: item.UE4SSABI, RCONPort: item.RCONPort})
|
items = append(items, RuntimeDLLExtensionPlanBody{Key: item.Key, Version: item.Version, ReleaseURL: item.ReleaseURL, Checksum: item.Checksum, SizeBytes: item.SizeBytes, TargetKey: item.TargetKey, ModKey: item.ModKey, DLLRef: item.DLLRef, TargetExecutableChecksum: item.TargetExecutableChecksum, UE4SSABI: item.UE4SSABI, RCONPort: item.RCONPort})
|
||||||
}
|
}
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGamePluginRuntimeProfilesBodyRejectsLegacyServerDeployments(t *testing.T) {
|
||||||
|
body := GamePluginRuntimeProfilesBody{ServerDeployments: json.RawMessage(`[{"key":"legacy"}]`)}
|
||||||
|
|
||||||
|
violations := body.UnsupportedLegacyProfileViolations("runtimeProfiles")
|
||||||
|
if len(violations) != 1 || !strings.Contains(violations[0], "runtimeProfiles.serverDeployments is no longer supported") {
|
||||||
|
t.Fatalf("expected legacy serverDeployments violation, got %v", violations)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGamePluginRuntimeProfilesBodyRejectsLegacyClientManagers(t *testing.T) {
|
||||||
|
body := GamePluginRuntimeProfilesBody{ClientManagers: json.RawMessage(`[{"key":"legacy"}]`), LifecycleProfiles: []RuntimeLifecycleProfileBody{{Key: "local", ClientManagerRef: json.RawMessage(`{"key":"legacy"}`)}}}
|
||||||
|
|
||||||
|
violations := body.UnsupportedLegacyProfileViolations("manifest.runtimeProfiles")
|
||||||
|
if len(violations) != 2 || !strings.Contains(strings.Join(violations, "; "), "clientManagers is no longer supported") || !strings.Contains(strings.Join(violations, "; "), "clientManagerRef is no longer supported") {
|
||||||
|
t.Fatalf("expected legacy client manager violations, got %v", violations)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGamePluginRuntimeProfilesBodyRejectsLegacyClientManagersWhenNull(t *testing.T) {
|
||||||
|
var body GamePluginRuntimeProfilesBody
|
||||||
|
if err := json.Unmarshal([]byte(`{"clientManagers":null}`), &body); err != nil {
|
||||||
|
t.Fatalf("decode runtime profile body: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
violations := body.UnsupportedLegacyProfileViolations("runtimeProfiles")
|
||||||
|
if len(violations) != 1 || !strings.Contains(violations[0], "runtimeProfiles.clientManagers is no longer supported") {
|
||||||
|
t.Fatalf("expected legacy clientManagers null violation, got %v", violations)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,8 +6,8 @@ Platform owns the reviewable dependency catalog, immutable plan digest, selected
|
|||||||
|
|
||||||
1. `GET /api/v1/server-instances/{id}/dependencies` resolves the installed plugin version, complete runtime binding, online Run endpoint OS/architecture, target-matched probes/plans, and canonical SHA-256 digest.
|
1. `GET /api/v1/server-instances/{id}/dependencies` resolves the installed plugin version, complete runtime binding, online Run endpoint OS/architecture, target-matched probes/plans, and canonical SHA-256 digest.
|
||||||
2. An install request must submit that exact digest. Platform re-resolves the declaration before creating `dependencies.install`; missing or stale plan evidence is denied and reported.
|
2. An install request must submit that exact digest. Platform re-resolves the declaration before creating `dependencies.install`; missing or stale plan evidence is denied and reported.
|
||||||
3. Run retrieves private input through signed `POST /api/v1/run/jobs/dependency-input` only for the active endpoint/session/attempt/lease and non-cancelled job. It executes closed command-version, Java, Docker, package, service, Steam, file, package-manager, verified HTTPS download, and SteamCMD adapters with bounded output/timeouts and a durable step journal.
|
3. Run retrieves private input through signed `POST /api/v1/run/jobs/dependency-input` only for the active endpoint/session/attempt/lease and non-cancelled job. It executes closed command-version, Java, Docker, package, service, file, package-manager, and verified HTTPS download adapters with bounded output/timeouts and a durable step journal.
|
||||||
4. Terminal evidence is typed and redacted. Platform verifies probe key, plan digest, result checksum, and job attempt before updating `DependencyStatus`.
|
4. Terminal evidence is typed dependency status, not log parsing. Platform verifies probe key, plan digest, result checksum, and job attempt before updating `DependencyStatus`.
|
||||||
|
|
||||||
## Self-update flow
|
## Self-update flow
|
||||||
|
|
||||||
|
|||||||
@@ -26,11 +26,9 @@ from command text. Empty `shell` means argv-oriented execution.
|
|||||||
|
|
||||||
Game plugins own concrete game policy: install/update commands, app ids,
|
Game plugins own concrete game policy: install/update commands, app ids,
|
||||||
executable refs, default launch flags, stop-before-update behavior, and startup
|
executable refs, default launch flags, stop-before-update behavior, and startup
|
||||||
argument construction. For SCUM, the plugin action assets own the SteamCMD flow:
|
argument construction. For SCUM, those values live in the SCUM plugin action
|
||||||
stop `SCUMServer.exe` when updating, keep SteamCMD outside the server install
|
assets and scripts; Platform and Run only pass the bounded deployment context to
|
||||||
root, run
|
the declared lifecycle action and execute it through generic action handling.
|
||||||
`steamcmd.exe +force_install_dir <serverRoot> +login anonymous +app_update 3792580 +quit`,
|
|
||||||
and start `<serverRoot>\\SCUM\\Binaries\\Win64\\SCUMServer.exe -port=<gamePort> -MaxPlayers=<maxPlayers> -log`.
|
|
||||||
|
|
||||||
Generated Run distributions carry validated plugin lifecycle assets into the
|
Generated Run distributions carry validated plugin lifecycle assets into the
|
||||||
server-scoped workspace. Run materializes those assets at startup and executes
|
server-scoped workspace. Run materializes those assets at startup and executes
|
||||||
|
|||||||
@@ -208,13 +208,6 @@ func (svc *CoreService) ReadRunUpdateChunk(request domain.RunUpdateChunkRequest)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.RunUpdateChunk{}, err
|
return domain.RunUpdateChunk{}, err
|
||||||
}
|
}
|
||||||
payload, err := svc.artifactPayload(artifact.ID)
|
|
||||||
if err != nil {
|
|
||||||
return domain.RunUpdateChunk{}, err
|
|
||||||
}
|
|
||||||
if int64(len(payload)) != artifact.SizeBytes || validator.BytesChecksum(payload) != artifact.Checksum {
|
|
||||||
return domain.RunUpdateChunk{}, validationError("update artifact content does not match metadata")
|
|
||||||
}
|
|
||||||
if request.Offset >= artifact.SizeBytes {
|
if request.Offset >= artifact.SizeBytes {
|
||||||
return domain.RunUpdateChunk{}, validationError("offset must be inside update artifact")
|
return domain.RunUpdateChunk{}, validationError("offset must be inside update artifact")
|
||||||
}
|
}
|
||||||
@@ -224,7 +217,11 @@ func (svc *CoreService) ReadRunUpdateChunk(request domain.RunUpdateChunkRequest)
|
|||||||
length = int(remaining)
|
length = int(remaining)
|
||||||
}
|
}
|
||||||
end := request.Offset + int64(length)
|
end := request.Offset + int64(length)
|
||||||
return domain.CopyRunUpdateChunk(domain.RunUpdateChunk{JobID: job.ID, ArtifactID: artifact.ID, Offset: request.Offset, TotalBytes: artifact.SizeBytes, Checksum: artifact.Checksum, Payload: payload[int(request.Offset):int(end)], Complete: end == artifact.SizeBytes}), nil
|
payload, err := svc.artifactStore.ReadPayloadRange(artifact.ID, request.Offset, length)
|
||||||
|
if err != nil {
|
||||||
|
return domain.RunUpdateChunk{}, err
|
||||||
|
}
|
||||||
|
return domain.CopyRunUpdateChunk(domain.RunUpdateChunk{JobID: job.ID, ArtifactID: artifact.ID, Offset: request.Offset, TotalBytes: artifact.SizeBytes, Checksum: artifact.Checksum, Payload: payload, Complete: end == artifact.SizeBytes}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svc *CoreService) activeFencedInputJob(endpointID, sessionToken, jobID, leaseToken string, attempt int) (domain.Job, error) {
|
func (svc *CoreService) activeFencedInputJob(endpointID, sessionToken, jobID, leaseToken string, attempt int) (domain.Job, error) {
|
||||||
|
|||||||
@@ -231,6 +231,10 @@ func TestRunUpdateTargetFencingChunksHealthAndRollbackProjection(t *testing.T) {
|
|||||||
if err != nil || string(chunk.Payload) != string(payload[:8]) || chunk.Offset != 0 || chunk.TotalBytes != int64(len(payload)) {
|
if err != nil || string(chunk.Payload) != string(payload[:8]) || chunk.Offset != 0 || chunk.TotalBytes != int64(len(payload)) {
|
||||||
t.Fatalf("read bounded update chunk: chunk=%+v err=%v", chunk, err)
|
t.Fatalf("read bounded update chunk: chunk=%+v err=%v", chunk, err)
|
||||||
}
|
}
|
||||||
|
secondChunk, err := svc.ReadRunUpdateChunk(domain.RunUpdateChunkRequest{RunEndpointID: instance.RunEndpointID, SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, Offset: 8, Length: 5})
|
||||||
|
if err != nil || string(secondChunk.Payload) != string(payload[8:13]) || secondChunk.Offset != 8 || secondChunk.Complete {
|
||||||
|
t.Fatalf("read offset update chunk: chunk=%+v err=%v", secondChunk, err)
|
||||||
|
}
|
||||||
if _, err := svc.ReadRunUpdateChunk(domain.RunUpdateChunkRequest{RunEndpointID: instance.RunEndpointID, SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt + 1, Offset: 0, Length: 8}); err == nil {
|
if _, err := svc.ReadRunUpdateChunk(domain.RunUpdateChunkRequest{RunEndpointID: instance.RunEndpointID, SessionToken: runSession, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt + 1, Offset: 0, Length: 8}); err == nil {
|
||||||
t.Fatal("expected stale update chunk attempt rejection")
|
t.Fatal("expected stale update chunk attempt rejection")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -339,7 +339,7 @@ func autonomousRunLogSourceKind(kind string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func autonomousDLLExtension(extension domain.RuntimeDLLExtensionPlan) domain.RunAutonomousDLLExtension {
|
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}
|
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, TargetExecutableChecksum: extension.TargetExecutableChecksum, UE4SSABI: extension.UE4SSABI, RCONPort: extension.RCONPort}
|
||||||
}
|
}
|
||||||
|
|
||||||
func autonomousDataTarget(target domain.RuntimeDataTarget) domain.RunAutonomousDataTarget {
|
func autonomousDataTarget(target domain.RuntimeDataTarget) domain.RunAutonomousDataTarget {
|
||||||
|
|||||||
@@ -782,7 +782,7 @@ func runReleasePlatformURL() string {
|
|||||||
if value := strings.TrimSpace(os.Getenv("PLATFORM_RUN_RELEASE_URL")); value != "" {
|
if value := strings.TrimSpace(os.Getenv("PLATFORM_RUN_RELEASE_URL")); value != "" {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
return "https://scum.npc0.com/"
|
return "http://127.0.0.1:8080/"
|
||||||
}
|
}
|
||||||
|
|
||||||
func distributionID(prefix string, parts ...interface{}) string {
|
func distributionID(prefix string, parts ...interface{}) string {
|
||||||
|
|||||||
@@ -973,6 +973,45 @@ func TestPluginFileWorkspaceDoesNotConstrainServerFileDispatch(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestReadRunFileInputChunkReadsRequestedRange(t *testing.T) {
|
||||||
|
svc := newTestCoreService()
|
||||||
|
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
|
||||||
|
plugin.FileWorkspace = scumTestFileWorkspace()
|
||||||
|
if err := svc.store.GamePlugins().Update(plugin); err != nil {
|
||||||
|
t.Fatalf("update plugin workspace: %v", err)
|
||||||
|
}
|
||||||
|
ownerSession := createServiceUserAndLogin(t, svc, domain.User{ID: "user-file-chunk", DisplayName: "File Chunk Owner", Email: "file-chunk@example.test", Roles: []string{"server-owner"}, PasswordHash: "secret-password"})
|
||||||
|
instance, err := svc.CreateServerInstanceForSession(ownerSession, domain.ServerInstance{ID: "server-file-chunk", PluginID: plugin.ID, RunEndpointID: endpoint.ID, Name: "File Chunk Server", State: domain.ServerInstanceStateRunning})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create server: %v", err)
|
||||||
|
}
|
||||||
|
createCompleteRuntimeBinding(t, svc, instance, "local")
|
||||||
|
payload := []byte("0123456789abcdef")
|
||||||
|
upload, err := svc.UploadServerFileForSession(ownerSession, domain.ServerFileUploadRequest{ServerInstanceID: instance.ID, DirectoryKey: "scum-logs", Filename: "chunked.log", Payload: payload, Checksum: validator.BytesChecksum(payload), IdempotencyKey: "file-chunk-upload"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("upload server file: %v", err)
|
||||||
|
}
|
||||||
|
hello := validRunControlHello()
|
||||||
|
hello.RunEndpointID = endpoint.ID
|
||||||
|
hello.CapabilityReport.Capabilities = []string{domain.JobCapabilityFilesWrite}
|
||||||
|
registered, err := svc.RegisterRunHello(hello)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("register run: %v", err)
|
||||||
|
}
|
||||||
|
claim, err := svc.ClaimRunJob(domain.RunJobClaim{RunEndpointID: endpoint.ID, SessionToken: registered.SessionToken, Capabilities: []string{domain.JobCapabilityFilesWrite}, Capacity: domain.RunCapacity{MaxJobs: 1}})
|
||||||
|
if err != nil || !claim.HasJob || claim.Job.JobID != upload.Job.ID {
|
||||||
|
t.Fatalf("claim file write: claim=%+v err=%v", claim, err)
|
||||||
|
}
|
||||||
|
chunk, err := svc.ReadRunFileInputChunk(domain.RunFileInputChunkRequest{RunEndpointID: endpoint.ID, SessionToken: registered.SessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, Offset: 4, Length: 6})
|
||||||
|
if err != nil || string(chunk.Payload) != string(payload[4:10]) || chunk.Offset != 4 || chunk.Complete {
|
||||||
|
t.Fatalf("read offset file input chunk: chunk=%+v err=%v", chunk, err)
|
||||||
|
}
|
||||||
|
finalChunk, err := svc.ReadRunFileInputChunk(domain.RunFileInputChunkRequest{RunEndpointID: endpoint.ID, SessionToken: registered.SessionToken, JobID: claim.Job.JobID, LeaseToken: claim.Job.LeaseToken, Attempt: claim.Job.Attempt, Offset: 14, Length: 8})
|
||||||
|
if err != nil || string(finalChunk.Payload) != string(payload[14:]) || !finalChunk.Complete {
|
||||||
|
t.Fatalf("read final file input chunk: chunk=%+v err=%v", finalChunk, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestServerFileListReportsFailedRuntimeRefresh(t *testing.T) {
|
func TestServerFileListReportsFailedRuntimeRefresh(t *testing.T) {
|
||||||
svc := newTestCoreService()
|
svc := newTestCoreService()
|
||||||
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
|
plugin, endpoint := createPluginAndRunEndpoint(t, svc)
|
||||||
|
|||||||
@@ -392,13 +392,6 @@ func (svc *CoreService) ReadRunFileInputChunk(request domain.RunFileInputChunkRe
|
|||||||
if artifact.OwnerKind != domain.ArtifactOwnerKindServerInstance || artifact.OwnerID != serverInstanceID || artifact.State != domain.ArtifactStateAvailable {
|
if artifact.OwnerKind != domain.ArtifactOwnerKindServerInstance || artifact.OwnerID != serverInstanceID || artifact.State != domain.ArtifactStateAvailable {
|
||||||
return domain.RunFileInputChunk{}, ErrForbidden
|
return domain.RunFileInputChunk{}, ErrForbidden
|
||||||
}
|
}
|
||||||
payload, err := svc.artifactPayload(artifact.ID)
|
|
||||||
if err != nil {
|
|
||||||
return domain.RunFileInputChunk{}, err
|
|
||||||
}
|
|
||||||
if int64(len(payload)) != artifact.SizeBytes || validator.BytesChecksum(payload) != artifact.Checksum {
|
|
||||||
return domain.RunFileInputChunk{}, validationError("file input artifact checksum mismatch")
|
|
||||||
}
|
|
||||||
if request.Offset >= artifact.SizeBytes {
|
if request.Offset >= artifact.SizeBytes {
|
||||||
return domain.RunFileInputChunk{}, validationError("offset must be inside artifact content")
|
return domain.RunFileInputChunk{}, validationError("offset must be inside artifact content")
|
||||||
}
|
}
|
||||||
@@ -407,8 +400,12 @@ func (svc *CoreService) ReadRunFileInputChunk(request domain.RunFileInputChunkRe
|
|||||||
if int64(length) > remaining {
|
if int64(length) > remaining {
|
||||||
length = int(remaining)
|
length = int(remaining)
|
||||||
}
|
}
|
||||||
end := int(request.Offset) + length
|
payload, err := svc.artifactStore.ReadPayloadRange(artifact.ID, request.Offset, length)
|
||||||
chunk := domain.RunFileInputChunk{JobID: job.ID, ArtifactID: artifact.ID, Offset: request.Offset, TotalBytes: artifact.SizeBytes, Checksum: artifact.Checksum, Payload: payload[int(request.Offset):end], Complete: int64(end) == artifact.SizeBytes}
|
if err != nil {
|
||||||
|
return domain.RunFileInputChunk{}, err
|
||||||
|
}
|
||||||
|
end := request.Offset + int64(length)
|
||||||
|
chunk := domain.RunFileInputChunk{JobID: job.ID, ArtifactID: artifact.ID, Offset: request.Offset, TotalBytes: artifact.SizeBytes, Checksum: artifact.Checksum, Payload: payload, Complete: end == artifact.SizeBytes}
|
||||||
return domain.CopyRunFileInputChunk(chunk), nil
|
return domain.CopyRunFileInputChunk(chunk), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -461,7 +461,7 @@ func lifecycleDLLExtensionPlans(profiles domain.GamePluginRuntimeProfiles, profi
|
|||||||
if !runtimeDLLExtensionSupportsTarget(extension, endpoint.Platform, endpoint.Architecture) {
|
if !runtimeDLLExtensionSupportsTarget(extension, endpoint.Platform, endpoint.Architecture) {
|
||||||
return nil, validationError("unsupported_extension_platform: UE4SS DLL requires windows/amd64")
|
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})
|
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, TargetExecutableChecksum: extension.TargetExecutableChecksum, UE4SSABI: extension.UE4SSABI, RCONPort: extension.RCONPort})
|
||||||
}
|
}
|
||||||
return plans, nil
|
return plans, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -466,7 +466,7 @@ func attachReadyLifecycleDLLExtension(t *testing.T, svc *CoreService, plugin *do
|
|||||||
Key: "scum-simple-rcon", DisplayName: "SCUM Simple RCON", Kind: "ue4ss-dll", Activation: "server-start", Version: "0.1.0", ReleaseState: "ready",
|
Key: "scum-simple-rcon", DisplayName: "SCUM Simple RCON", Kind: "ue4ss-dll", Activation: "server-start", Version: "0.1.0", ReleaseState: "ready",
|
||||||
ReleaseURL: "https://cdn.npc0.com/scum_simple_rcon_ue4s.dll", Checksum: "sha256:" + strings.Repeat("a", 64), SizeBytes: 1024,
|
ReleaseURL: "https://cdn.npc0.com/scum_simple_rcon_ue4s.dll", Checksum: "sha256:" + strings.Repeat("a", 64), SizeBytes: 1024,
|
||||||
TargetKey: "ue4ss/scum-simple-rcon", ModKey: "scum_simple_rcon", DLLRef: "ue4ss/Mods/scum_simple_rcon/dlls/main.dll",
|
TargetKey: "ue4ss/scum-simple-rcon", ModKey: "scum_simple_rcon", DLLRef: "ue4ss/Mods/scum_simple_rcon/dlls/main.dll",
|
||||||
SCUMExecutableChecksum: "sha256:" + strings.Repeat("b", 64), UE4SSABI: "ue4ss-3.0", SupportedTargets: []domain.RuntimeTarget{{OS: "windows", Arch: "amd64"}}, UpdateOnStart: true, RCONPort: 27015,
|
TargetExecutableChecksum: "sha256:" + strings.Repeat("b", 64), UE4SSABI: "ue4ss-3.0", SupportedTargets: []domain.RuntimeTarget{{OS: "windows", Arch: "amd64"}}, UpdateOnStart: true, RCONPort: 27015,
|
||||||
}}
|
}}
|
||||||
if err := svc.store.GamePlugins().Update(*plugin); err != nil {
|
if err := svc.store.GamePlugins().Update(*plugin); err != nil {
|
||||||
t.Fatalf("attach ready DLL extension: %v", err)
|
t.Fatalf("attach ready DLL extension: %v", err)
|
||||||
|
|||||||
@@ -280,7 +280,7 @@ func newSourceRCONFixtureWithRuntimeBinding(t *testing.T, createBinding bool) (*
|
|||||||
Key: "scum-simple-rcon", DisplayName: "SCUM Simple RCON", Kind: "ue4ss-dll", Activation: "server-start", Version: "0.1.0", ReleaseState: "ready",
|
Key: "scum-simple-rcon", DisplayName: "SCUM Simple RCON", Kind: "ue4ss-dll", Activation: "server-start", Version: "0.1.0", ReleaseState: "ready",
|
||||||
ReleaseURL: "https://cdn.npc0.com/scum_simple_rcon_ue4s.dll", Checksum: "sha256:" + strings.Repeat("a", 64), SizeBytes: 1024,
|
ReleaseURL: "https://cdn.npc0.com/scum_simple_rcon_ue4s.dll", Checksum: "sha256:" + strings.Repeat("a", 64), SizeBytes: 1024,
|
||||||
TargetKey: "ue4ss/scum-simple-rcon", ModKey: "scum_simple_rcon", DLLRef: "ue4ss/Mods/scum_simple_rcon/dlls/main.dll",
|
TargetKey: "ue4ss/scum-simple-rcon", ModKey: "scum_simple_rcon", DLLRef: "ue4ss/Mods/scum_simple_rcon/dlls/main.dll",
|
||||||
SCUMExecutableChecksum: "sha256:" + strings.Repeat("b", 64), UE4SSABI: "ue4ss-3.0", SupportedTargets: []domain.RuntimeTarget{{OS: "windows", Arch: "amd64"}}, UpdateOnStart: true, RCONPort: 27015,
|
TargetExecutableChecksum: "sha256:" + strings.Repeat("b", 64), UE4SSABI: "ue4ss-3.0", SupportedTargets: []domain.RuntimeTarget{{OS: "windows", Arch: "amd64"}}, UpdateOnStart: true, RCONPort: 27015,
|
||||||
}},
|
}},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ func TestValidateGamePluginRuntimeProfilesRejectsUnsafeOrUnpublishedDLLExtension
|
|||||||
func TestValidateJobRejectsDLLPlanOutsideProcessStart(t *testing.T) {
|
func TestValidateJobRejectsDLLPlanOutsideProcessStart(t *testing.T) {
|
||||||
profiles := validRuntimeDLLExtensionProfiles()
|
profiles := validRuntimeDLLExtensionProfiles()
|
||||||
extension := profiles.DLLExtensions[0]
|
extension := profiles.DLLExtensions[0]
|
||||||
job := domain.Job{ID: "dll-job", ServerInstanceID: "server-1", RunEndpointID: "run-1", Capability: domain.LifecycleCapabilityStop, IdempotencyKey: "dll-stop", State: domain.JobStateQueued, RetryPolicy: domain.JobRetryPolicy{MaxAttempts: 1, InitialBackoffSeconds: 1, MaxBackoffSeconds: 1}, ExecutionInput: domain.JobExecutionInput{LifecycleOperation: "stop", DLLExtensions: []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}}}}
|
job := domain.Job{ID: "dll-job", ServerInstanceID: "server-1", RunEndpointID: "run-1", Capability: domain.LifecycleCapabilityStop, IdempotencyKey: "dll-stop", State: domain.JobStateQueued, RetryPolicy: domain.JobRetryPolicy{MaxAttempts: 1, InitialBackoffSeconds: 1, MaxBackoffSeconds: 1}, ExecutionInput: domain.JobExecutionInput{LifecycleOperation: "stop", DLLExtensions: []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, TargetExecutableChecksum: extension.TargetExecutableChecksum, UE4SSABI: extension.UE4SSABI, RCONPort: extension.RCONPort}}}}
|
||||||
if err := ValidateJob(job); err == nil || !strings.Contains(err.Error(), "process.start") {
|
if err := ValidateJob(job); err == nil || !strings.Contains(err.Error(), "process.start") {
|
||||||
t.Fatalf("expected process.start plan restriction, got %v", err)
|
t.Fatalf("expected process.start plan restriction, got %v", err)
|
||||||
}
|
}
|
||||||
@@ -65,7 +65,7 @@ func validRuntimeDLLExtensionProfiles() domain.GamePluginRuntimeProfiles {
|
|||||||
Key: "scum-simple-rcon", DisplayName: "SCUM Simple RCON", Kind: "ue4ss-dll", Activation: "server-start", Version: "0.1.0", ReleaseState: "ready",
|
Key: "scum-simple-rcon", DisplayName: "SCUM Simple RCON", Kind: "ue4ss-dll", Activation: "server-start", Version: "0.1.0", ReleaseState: "ready",
|
||||||
ReleaseURL: "https://cdn.npc0.com/scum_simple_rcon_ue4s.dll", Checksum: "sha256:" + strings.Repeat("a", 64), SizeBytes: 1024,
|
ReleaseURL: "https://cdn.npc0.com/scum_simple_rcon_ue4s.dll", Checksum: "sha256:" + strings.Repeat("a", 64), SizeBytes: 1024,
|
||||||
TargetKey: "ue4ss/scum-simple-rcon", ModKey: "scum_simple_rcon", DLLRef: "ue4ss/Mods/scum_simple_rcon/dlls/main.dll",
|
TargetKey: "ue4ss/scum-simple-rcon", ModKey: "scum_simple_rcon", DLLRef: "ue4ss/Mods/scum_simple_rcon/dlls/main.dll",
|
||||||
SCUMExecutableChecksum: "sha256:" + strings.Repeat("b", 64), UE4SSABI: "ue4ss-3.0", SupportedTargets: []domain.RuntimeTarget{{OS: "windows", Arch: "amd64"}}, UpdateOnStart: true, RCONPort: 27015,
|
TargetExecutableChecksum: "sha256:" + strings.Repeat("b", 64), UE4SSABI: "ue4ss-3.0", SupportedTargets: []domain.RuntimeTarget{{OS: "windows", Arch: "amd64"}}, UpdateOnStart: true, RCONPort: 27015,
|
||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ func ValidateGamePluginRuntimeProfiles(profiles domain.GamePluginRuntimeProfiles
|
|||||||
discoveryKeys := map[string]struct{}{}
|
discoveryKeys := map[string]struct{}{}
|
||||||
dependencyKeys := map[string]struct{}{}
|
dependencyKeys := map[string]struct{}{}
|
||||||
installPlanKeys := map[string]struct{}{}
|
installPlanKeys := map[string]struct{}{}
|
||||||
serverDeploymentKeys := map[string]struct{}{}
|
|
||||||
logSourceKeys := map[string]struct{}{}
|
logSourceKeys := map[string]struct{}{}
|
||||||
|
|
||||||
for i, probe := range profiles.Discovery {
|
for i, probe := range profiles.Discovery {
|
||||||
@@ -95,7 +94,7 @@ func ValidateGamePluginRuntimeProfiles(profiles domain.GamePluginRuntimeProfiles
|
|||||||
}
|
}
|
||||||
for j, step := range plan.Steps {
|
for j, step := range plan.Steps {
|
||||||
stepPrefix := fmt.Sprintf("%s.steps[%d]", prefix, j)
|
stepPrefix := fmt.Sprintf("%s.steps[%d]", prefix, j)
|
||||||
if !oneOf(step.Type, "package", "verified-download", "steamcmd-app", "manual") {
|
if !oneOf(step.Type, "package", "verified-download", "manual") {
|
||||||
violations = append(violations, stepPrefix+".type is invalid")
|
violations = append(violations, stepPrefix+".type is invalid")
|
||||||
}
|
}
|
||||||
violations = append(violations, validateProfileKey(stepPrefix+".targetKey", step.TargetKey)...)
|
violations = append(violations, validateProfileKey(stepPrefix+".targetKey", step.TargetKey)...)
|
||||||
@@ -133,100 +132,14 @@ func ValidateGamePluginRuntimeProfiles(profiles domain.GamePluginRuntimeProfiles
|
|||||||
if step.DownloadRef == "" || step.Checksum == "" {
|
if step.DownloadRef == "" || step.Checksum == "" {
|
||||||
violations = append(violations, stepPrefix+" requires downloadRef and checksum")
|
violations = append(violations, stepPrefix+" requires downloadRef and checksum")
|
||||||
}
|
}
|
||||||
case "steamcmd-app":
|
|
||||||
if step.PackageManager != "" && step.PackageManager != "steamcmd" || !regexp.MustCompile(`^[0-9]{1,12}$`).MatchString(step.PackageName) {
|
|
||||||
violations = append(violations, stepPrefix+" requires a numeric Steam app and steamcmd adapter")
|
|
||||||
}
|
|
||||||
case "manual":
|
case "manual":
|
||||||
if step.DownloadRef != "" || step.Checksum != "" || step.PackageName != "" {
|
if step.DownloadRef != "" || step.Checksum != "" || step.PackageManager != "" || step.PackageName != "" || step.Version != "" {
|
||||||
violations = append(violations, stepPrefix+" manual step cannot contain machine execution fields")
|
violations = append(violations, stepPrefix+" manual step cannot contain machine execution fields")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
violations = append(violations, validateRuntimePlatforms(prefix+".platforms", plan.Platforms)...)
|
violations = append(violations, validateRuntimePlatforms(prefix+".platforms", plan.Platforms)...)
|
||||||
}
|
}
|
||||||
for i, profile := range profiles.ServerDeployments {
|
|
||||||
prefix := fmt.Sprintf("runtimeProfiles.serverDeployments[%d]", i)
|
|
||||||
violations = append(violations, validateProfileKey(prefix+".key", profile.Key)...)
|
|
||||||
violations = append(violations, recordRuntimeProfileKey(serverDeploymentKeys, prefix+".key", profile.Key)...)
|
|
||||||
if !validSemanticVersion(profile.Version) {
|
|
||||||
violations = append(violations, prefix+".version must be semantic")
|
|
||||||
}
|
|
||||||
if !regexp.MustCompile(`^[0-9]{1,12}$`).MatchString(profile.SteamAppID) {
|
|
||||||
violations = append(violations, prefix+".steamAppId must be numeric")
|
|
||||||
}
|
|
||||||
for field, value := range map[string]string{"executableKey": profile.ExecutableKey, "installRootKey": profile.InstallRootKey, "configKey": profile.ConfigKey} {
|
|
||||||
violations = append(violations, validateProfileKey(prefix+"."+field, value)...)
|
|
||||||
}
|
|
||||||
if profile.ConfigFormat != "ini" && profile.ConfigFormat != "json" && profile.ConfigFormat != "yaml" && profile.ConfigFormat != "properties" {
|
|
||||||
violations = append(violations, prefix+".configFormat is invalid")
|
|
||||||
}
|
|
||||||
if len(profile.SupportedTargets) == 0 {
|
|
||||||
violations = append(violations, prefix+".supportedTargets must not be empty")
|
|
||||||
}
|
|
||||||
prerequisiteKeys := map[string]struct{}{}
|
|
||||||
for j, prerequisite := range profile.Prerequisites {
|
|
||||||
prerequisitePrefix := fmt.Sprintf("%s.prerequisites[%d]", prefix, j)
|
|
||||||
violations = append(violations, validateProfileKey(prerequisitePrefix+".key", prerequisite.Key)...)
|
|
||||||
if _, exists := prerequisiteKeys[prerequisite.Key]; exists {
|
|
||||||
violations = append(violations, prerequisitePrefix+".key duplicates another prerequisite")
|
|
||||||
}
|
|
||||||
prerequisiteKeys[prerequisite.Key] = struct{}{}
|
|
||||||
if !oneOf(prerequisite.Kind, "steamcmd", "windows-vcredist", "windows-directx") {
|
|
||||||
violations = append(violations, prerequisitePrefix+".kind is invalid")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for j, target := range profile.SupportedTargets {
|
|
||||||
if !validPluginSupportedOS(target.OS) || !oneOf(target.Arch, "amd64", "arm64") {
|
|
||||||
violations = append(violations, fmt.Sprintf("%s.supportedTargets[%d] is invalid", prefix, j))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mappingKeys := map[string]struct{}{}
|
|
||||||
for j, mapping := range profile.ConfigMappings {
|
|
||||||
mappingPrefix := fmt.Sprintf("%s.configMappings[%d]", prefix, j)
|
|
||||||
if !regexp.MustCompile(`^[A-Za-z][A-Za-z0-9._/-]{0,79}$`).MatchString(mapping.FieldKey) {
|
|
||||||
violations = append(violations, mappingPrefix+".fieldKey is invalid")
|
|
||||||
}
|
|
||||||
violations = append(violations, validateProfileKey(mappingPrefix+".configKey", mapping.ConfigKey)...)
|
|
||||||
if _, exists := mappingKeys[mapping.FieldKey]; exists {
|
|
||||||
violations = append(violations, mappingPrefix+".fieldKey duplicates another mapping")
|
|
||||||
}
|
|
||||||
mappingKeys[mapping.FieldKey] = struct{}{}
|
|
||||||
if !oneOf(mapping.ValueType, "text", "integer", "number", "boolean", "port") {
|
|
||||||
violations = append(violations, mappingPrefix+".valueType is invalid")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
markerKeys := map[string]struct{}{}
|
|
||||||
for j, marker := range profile.DiscoveryMarkers {
|
|
||||||
markerPrefix := fmt.Sprintf("%s.discoveryMarkers[%d]", prefix, j)
|
|
||||||
violations = append(violations, validateProfileKey(markerPrefix+".key", marker.Key)...)
|
|
||||||
violations = append(violations, validateProfileKey(markerPrefix+".targetKey", marker.TargetKey)...)
|
|
||||||
if _, exists := markerKeys[marker.Key]; exists {
|
|
||||||
violations = append(violations, markerPrefix+".key duplicates another marker")
|
|
||||||
}
|
|
||||||
markerKeys[marker.Key] = struct{}{}
|
|
||||||
if !oneOf(marker.Kind, "file.exists", "command.version", "port.open", "steam.app") {
|
|
||||||
violations = append(violations, markerPrefix+".kind is invalid")
|
|
||||||
}
|
|
||||||
violations = append(violations, validateSafeRuntimeValue(markerPrefix+".expected", marker.Expected)...)
|
|
||||||
}
|
|
||||||
checkKeys := map[string]struct{}{}
|
|
||||||
for j, check := range profile.VerificationChecks {
|
|
||||||
checkPrefix := fmt.Sprintf("%s.verificationChecks[%d]", prefix, j)
|
|
||||||
violations = append(violations, validateProfileKey(checkPrefix+".key", check.Key)...)
|
|
||||||
violations = append(violations, validateProfileKey(checkPrefix+".targetKey", check.TargetKey)...)
|
|
||||||
if _, exists := checkKeys[check.Key]; exists {
|
|
||||||
violations = append(violations, checkPrefix+".key duplicates another check")
|
|
||||||
}
|
|
||||||
checkKeys[check.Key] = struct{}{}
|
|
||||||
if !oneOf(check.Kind, "executable.present", "version.matches", "port.bound", "config.readable", "process.healthy") {
|
|
||||||
violations = append(violations, checkPrefix+".kind is invalid")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(profile.VerificationChecks) == 0 || !containsRequiredVerification(profile.VerificationChecks) {
|
|
||||||
violations = append(violations, prefix+".verificationChecks must include executable, config, port, and process checks")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for i, source := range profiles.LogSources {
|
for i, source := range profiles.LogSources {
|
||||||
prefix := fmt.Sprintf("runtimeProfiles.logSources[%d]", i)
|
prefix := fmt.Sprintf("runtimeProfiles.logSources[%d]", i)
|
||||||
violations = append(violations, validateProfileKey(prefix+".key", source.Key)...)
|
violations = append(violations, validateProfileKey(prefix+".key", source.Key)...)
|
||||||
@@ -329,23 +242,6 @@ func ValidateGamePluginRuntimeProfiles(profiles domain.GamePluginRuntimeProfiles
|
|||||||
return finish(violations)
|
return finish(violations)
|
||||||
}
|
}
|
||||||
|
|
||||||
func containsRequiredVerification(checks []domain.RuntimeServerVerificationCheck) bool {
|
|
||||||
required := map[string]bool{"executable.present": false, "port.bound": false, "config.readable": false, "process.healthy": false}
|
|
||||||
for _, check := range checks {
|
|
||||||
if check.Required {
|
|
||||||
if _, ok := required[check.Kind]; ok {
|
|
||||||
required[check.Kind] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, present := range required {
|
|
||||||
if !present {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateRuntimeDLLExtensionProfile(prefix string, extension domain.RuntimeDLLExtensionProfile) []string {
|
func validateRuntimeDLLExtensionProfile(prefix string, extension domain.RuntimeDLLExtensionProfile) []string {
|
||||||
var violations []string
|
var violations []string
|
||||||
if extension.Kind != "ue4ss-dll" || extension.Activation != "server-start" {
|
if extension.Kind != "ue4ss-dll" || extension.Activation != "server-start" {
|
||||||
@@ -381,8 +277,8 @@ func validateRuntimeDLLExtensionProfile(prefix string, extension domain.RuntimeD
|
|||||||
if extension.ReleaseURL == "" {
|
if extension.ReleaseURL == "" {
|
||||||
violations = append(violations, prefix+".releaseUrl is required for a ready release")
|
violations = append(violations, prefix+".releaseUrl is required for a ready release")
|
||||||
}
|
}
|
||||||
if !validSHA256Checksum(extension.Checksum) || !validSHA256Checksum(extension.SCUMExecutableChecksum) {
|
if !validSHA256Checksum(extension.Checksum) || !validSHA256Checksum(extension.TargetExecutableChecksum) {
|
||||||
violations = append(violations, prefix+".checksum and scumExecutableChecksum must be SHA-256")
|
violations = append(violations, prefix+".checksum and targetExecutableChecksum must be SHA-256")
|
||||||
}
|
}
|
||||||
if extension.SizeBytes < 1 || extension.SizeBytes > 128*1024*1024 {
|
if extension.SizeBytes < 1 || extension.SizeBytes > 128*1024*1024 {
|
||||||
violations = append(violations, prefix+".sizeBytes is out of bounds")
|
violations = append(violations, prefix+".sizeBytes is out of bounds")
|
||||||
@@ -409,22 +305,22 @@ func validateRuntimeDLLReleaseURL(field string, value string) []string {
|
|||||||
|
|
||||||
func validateRuntimeDLLExtensionPlan(prefix string, plan domain.RuntimeDLLExtensionPlan) []string {
|
func validateRuntimeDLLExtensionPlan(prefix string, plan domain.RuntimeDLLExtensionPlan) []string {
|
||||||
return validateRuntimeDLLExtensionProfile(prefix, domain.RuntimeDLLExtensionProfile{
|
return validateRuntimeDLLExtensionProfile(prefix, domain.RuntimeDLLExtensionProfile{
|
||||||
Key: plan.Key,
|
Key: plan.Key,
|
||||||
Kind: "ue4ss-dll",
|
Kind: "ue4ss-dll",
|
||||||
Activation: "server-start",
|
Activation: "server-start",
|
||||||
Version: plan.Version,
|
Version: plan.Version,
|
||||||
ReleaseState: "ready",
|
ReleaseState: "ready",
|
||||||
ReleaseURL: plan.ReleaseURL,
|
ReleaseURL: plan.ReleaseURL,
|
||||||
Checksum: plan.Checksum,
|
Checksum: plan.Checksum,
|
||||||
SizeBytes: plan.SizeBytes,
|
SizeBytes: plan.SizeBytes,
|
||||||
TargetKey: plan.TargetKey,
|
TargetKey: plan.TargetKey,
|
||||||
ModKey: plan.ModKey,
|
ModKey: plan.ModKey,
|
||||||
DLLRef: plan.DLLRef,
|
DLLRef: plan.DLLRef,
|
||||||
SCUMExecutableChecksum: plan.SCUMExecutableChecksum,
|
TargetExecutableChecksum: plan.TargetExecutableChecksum,
|
||||||
UE4SSABI: plan.UE4SSABI,
|
UE4SSABI: plan.UE4SSABI,
|
||||||
SupportedTargets: []domain.RuntimeTarget{{OS: "windows", Arch: "amd64"}},
|
SupportedTargets: []domain.RuntimeTarget{{OS: "windows", Arch: "amd64"}},
|
||||||
UpdateOnStart: true,
|
UpdateOnStart: true,
|
||||||
RCONPort: plan.RCONPort,
|
RCONPort: plan.RCONPort,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package validator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"browser.local/platform/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestValidateGamePluginRuntimeProfilesRejectsLegacySteamCMDAppStep(t *testing.T) {
|
||||||
|
profiles := domain.GamePluginRuntimeProfiles{InstallPlans: []domain.RuntimeInstallPlan{{Key: "install-game", Title: "Install game", Steps: []domain.RuntimeInstallStep{{Type: "steamcmd-app", TargetKey: "server/install-root", PackageManager: "steamcmd", PackageName: "123456"}}}}}
|
||||||
|
|
||||||
|
err := ValidateGamePluginRuntimeProfiles(profiles)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "type is invalid") {
|
||||||
|
t.Fatalf("expected legacy steamcmd-app step rejection, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateGamePluginRuntimeProfilesRejectsManualStepExecutionFields(t *testing.T) {
|
||||||
|
profiles := domain.GamePluginRuntimeProfiles{InstallPlans: []domain.RuntimeInstallPlan{{Key: "manual-plan", Title: "Manual plan", Steps: []domain.RuntimeInstallStep{{Type: "manual", TargetKey: "operator/manual", PackageManager: "steamcmd"}}}}}
|
||||||
|
|
||||||
|
err := ValidateGamePluginRuntimeProfiles(profiles)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "manual step cannot contain machine execution fields") {
|
||||||
|
t.Fatalf("expected manual execution field rejection, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -265,20 +265,6 @@ export interface RuntimeInstallPlanResponse {
|
|||||||
steps: Array<{ type: string; targetKey: string; packageManager?: string; packageName?: string; version?: string; downloadRef?: string; checksum?: string }>;
|
steps: Array<{ type: string; targetKey: string; packageManager?: string; packageName?: string; version?: string; downloadRef?: string; checksum?: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RuntimeServerDeploymentProfileResponse {
|
|
||||||
key: string;
|
|
||||||
version: string;
|
|
||||||
supportedTargets: Array<{ os: string; arch: string }>;
|
|
||||||
steamAppId: string;
|
|
||||||
executableKey: string;
|
|
||||||
installRootKey: string;
|
|
||||||
configKey: string;
|
|
||||||
configFormat: string;
|
|
||||||
configMappings: Array<{ fieldKey: string; configKey: string; valueType: string; required?: boolean }>;
|
|
||||||
discoveryMarkers: Array<{ key: string; kind: string; targetKey: string; expected?: string; required?: boolean }>;
|
|
||||||
verificationChecks: Array<{ key: string; kind: string; targetKey: string; required?: boolean }>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RuntimeLogSourceResponse {
|
export interface RuntimeLogSourceResponse {
|
||||||
key: string;
|
key: string;
|
||||||
kind: string;
|
kind: string;
|
||||||
@@ -311,7 +297,7 @@ export interface RuntimeDLLExtensionProfileResponse {
|
|||||||
releaseFilename?: string;
|
releaseFilename?: string;
|
||||||
checksum?: string;
|
checksum?: string;
|
||||||
sizeBytes?: number;
|
sizeBytes?: number;
|
||||||
scumExecutableChecksum?: string;
|
targetExecutableChecksum?: string;
|
||||||
ue4ssAbi?: string;
|
ue4ssAbi?: string;
|
||||||
supportedTargets: Array<{ os: string; arch: string }>;
|
supportedTargets: Array<{ os: string; arch: string }>;
|
||||||
updateOnStart: boolean;
|
updateOnStart: boolean;
|
||||||
@@ -322,7 +308,6 @@ export interface GamePluginRuntimeProfilesResponse {
|
|||||||
lifecycleProfiles?: RuntimeLifecycleProfileResponse[];
|
lifecycleProfiles?: RuntimeLifecycleProfileResponse[];
|
||||||
dependencyProbes?: RuntimeDependencyProbeResponse[];
|
dependencyProbes?: RuntimeDependencyProbeResponse[];
|
||||||
installPlans?: RuntimeInstallPlanResponse[];
|
installPlans?: RuntimeInstallPlanResponse[];
|
||||||
serverDeployments?: RuntimeServerDeploymentProfileResponse[];
|
|
||||||
logSources?: RuntimeLogSourceResponse[];
|
logSources?: RuntimeLogSourceResponse[];
|
||||||
transportProfiles?: RuntimeTransportProfileResponse[];
|
transportProfiles?: RuntimeTransportProfileResponse[];
|
||||||
dllExtensions?: RuntimeDLLExtensionProfileResponse[];
|
dllExtensions?: RuntimeDLLExtensionProfileResponse[];
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ function RuntimeDLLExtensionsContent({ extensions }: { extensions: RuntimeDLLExt
|
|||||||
<h2>UE4SS DLL 扩展</h2>
|
<h2>UE4SS DLL 扩展</h2>
|
||||||
<span className="page-status">声明式、固定版本、启动前同步</span>
|
<span className="page-status">声明式、固定版本、启动前同步</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="page-status">Run 不直接执行 DLL;已安装的 UE4SS 会在 SCUM 正常启动时加载它。</p>
|
<p className="page-status">Run 不直接执行 DLL;已安装的 UE4SS 会在目标服务端正常启动时加载它。</p>
|
||||||
<div className="console-record-list">
|
<div className="console-record-list">
|
||||||
{extensions.map((extension) => (
|
{extensions.map((extension) => (
|
||||||
<article key={extension.key} className="console-record">
|
<article key={extension.key} className="console-record">
|
||||||
@@ -43,8 +43,8 @@ function RuntimeDLLExtensionsContent({ extensions }: { extensions: RuntimeDLLExt
|
|||||||
<dd>{extension.checksum || "待发布后固定"}</dd>
|
<dd>{extension.checksum || "待发布后固定"}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt>SCUM 兼容</dt>
|
<dt>目标可执行文件兼容</dt>
|
||||||
<dd>{extension.scumExecutableChecksum || "待发布后固定"}</dd>
|
<dd>{extension.targetExecutableChecksum || "待发布后固定"}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt>UE4SS ABI</dt>
|
<dt>UE4SS ABI</dt>
|
||||||
@@ -53,7 +53,7 @@ function RuntimeDLLExtensionsContent({ extensions }: { extensions: RuntimeDLLExt
|
|||||||
</dl>
|
</dl>
|
||||||
<div className="tag-list" aria-label={`${extension.displayName} activation policy`}>
|
<div className="tag-list" aria-label={`${extension.displayName} activation policy`}>
|
||||||
<span>启动前自动校验和更新</span>
|
<span>启动前自动校验和更新</span>
|
||||||
<span>运行:SCUM 服务器受监管启动</span>
|
<span>运行:目标服务端受监管启动</span>
|
||||||
<span>由 UE4SS 正常加载</span>
|
<span>由 UE4SS 正常加载</span>
|
||||||
<span>{supportedTargets(extension)}</span>
|
<span>{supportedTargets(extension)}</span>
|
||||||
<span>Linux 启动前拒绝</span>
|
<span>Linux 启动前拒绝</span>
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ export function ServerDeploymentWorkflow({ open, kind, plugins, initialForm, dep
|
|||||||
const [revealError, setRevealError] = useState("");
|
const [revealError, setRevealError] = useState("");
|
||||||
const selectedPlugin = useMemo(() => plugins.find((plugin) => plugin.id === form.pluginId), [form.pluginId, plugins]);
|
const selectedPlugin = useMemo(() => plugins.find((plugin) => plugin.id === form.pluginId), [form.pluginId, plugins]);
|
||||||
const pluginFields = selectedPlugin?.createFields ?? [];
|
const pluginFields = selectedPlugin?.createFields ?? [];
|
||||||
const isScum = selectedPlugin?.id === "game.scum";
|
|
||||||
const workflowSteps = kind === "create"
|
const workflowSteps = kind === "create"
|
||||||
? [{ label: "基本信息", icon: Compass }, { label: "部署方式", icon: ServerCog }, { label: "相关配置", icon: FolderCog }, { label: "确认", icon: Rocket }]
|
? [{ label: "基本信息", icon: Compass }, { label: "部署方式", icon: ServerCog }, { label: "相关配置", icon: FolderCog }, { label: "确认", icon: Rocket }]
|
||||||
: [{ label: "相关配置", icon: FolderCog }, { label: "确认", icon: Rocket }];
|
: [{ label: "相关配置", icon: FolderCog }, { label: "确认", icon: Rocket }];
|
||||||
@@ -66,7 +65,6 @@ export function ServerDeploymentWorkflow({ open, kind, plugins, initialForm, dep
|
|||||||
if (step === pluginStep) return Boolean(form.pluginId) && Boolean(form.name.trim());
|
if (step === pluginStep) return Boolean(form.pluginId) && Boolean(form.name.trim());
|
||||||
if (step === modeStep) return Boolean(form.deploymentMode);
|
if (step === modeStep) return Boolean(form.deploymentMode);
|
||||||
if (step === configurationStep) {
|
if (step === configurationStep) {
|
||||||
if (isScum && form.deploymentMode === "guided-install" && !form.serverRoot.trim() && !deployment?.serverRootConfigured) return false;
|
|
||||||
if (form.deploymentMode === "existing-server" && !form.serverRoot.trim() && !deployment?.serverRootConfigured) return false;
|
if (form.deploymentMode === "existing-server" && !form.serverRoot.trim() && !deployment?.serverRootConfigured) return false;
|
||||||
if (form.deploymentMode === "custom-command" && !form.startCommand.trim() && !deployment?.startCommandConfigured) return false;
|
if (form.deploymentMode === "custom-command" && !form.startCommand.trim() && !deployment?.startCommandConfigured) return false;
|
||||||
return form.deploymentMode !== "guided-install" || pluginFields.filter((field) => field.required).every((field) => Boolean(form.createInputs[field.key]?.trim()));
|
return form.deploymentMode !== "guided-install" || pluginFields.filter((field) => field.required).every((field) => Boolean(form.createInputs[field.key]?.trim()));
|
||||||
@@ -111,14 +109,14 @@ export function ServerDeploymentWorkflow({ open, kind, plugins, initialForm, dep
|
|||||||
<div className="workflow-hint-grid"><div className="workflow-hint-card"><strong>创建基础信息</strong><span>插件决定下一步显示哪些部署方式和游戏参数。</span></div><div className="workflow-hint-card"><strong>配置启动项</strong><span>新建安装、接管已有和自定义启动分别填写自己的字段。</span></div><div className="workflow-hint-card"><strong>平台构建 Run 包</strong><span>平台在自有构建器中打包,Run 启动后自动上报心跳。</span></div></div>
|
<div className="workflow-hint-grid"><div className="workflow-hint-card"><strong>创建基础信息</strong><span>插件决定下一步显示哪些部署方式和游戏参数。</span></div><div className="workflow-hint-card"><strong>配置启动项</strong><span>新建安装、接管已有和自定义启动分别填写自己的字段。</span></div><div className="workflow-hint-card"><strong>平台构建 Run 包</strong><span>平台在自有构建器中打包,Run 启动后自动上报心跳。</span></div></div>
|
||||||
<div className="form-grid"><label>插件类型<select name="pluginId" value={form.pluginId} onChange={updateForm} required>{plugins.map((plugin) => <option key={plugin.id} value={plugin.id}>{pluginLabel(plugin, plugin.id)}</option>)}</select></label><label>服务器名称<input name="name" value={form.name} onChange={updateForm} placeholder="Example Survival #3" required /></label></div>
|
<div className="form-grid"><label>插件类型<select name="pluginId" value={form.pluginId} onChange={updateForm} required>{plugins.map((plugin) => <option key={plugin.id} value={plugin.id}>{pluginLabel(plugin, plugin.id)}</option>)}</select></label><label>服务器名称<input name="name" value={form.name} onChange={updateForm} placeholder="Example Survival #3" required /></label></div>
|
||||||
</div>}
|
</div>}
|
||||||
{step === modeStep && <div className="deployment-workflow-body"><p className="section-copy">选择这台服务器的创建方式;下一步只显示该方式需要的启动项。</p>{isScum && <div className="form-guidance"><strong>SCUM 自动部署链路</strong><span>Run 会按预检 → 安装或扫描 → 配置映射 → 健康验证执行;目录本身不代表安装完成。</span></div>}<div className="deployment-mode-grid">
|
{step === modeStep && <div className="deployment-workflow-body"><p className="section-copy">选择这台服务器的创建方式;下一步只显示该方式需要的启动项。</p><div className="form-guidance"><strong>插件生命周期链路</strong><span>Run 会按插件声明执行预检、安装或扫描、配置映射与健康验证;目录本身不代表安装完成。</span></div><div className="deployment-mode-grid">
|
||||||
<ModeOption active={form.deploymentMode === "guided-install"} title="新建并安装" copy="按插件的推荐方案安装并写入游戏配置。适合绝大多数新服务器。" onClick={() => setForm((current) => ({ ...current, deploymentMode: "guided-install" }))} />
|
<ModeOption active={form.deploymentMode === "guided-install"} title="新建并安装" copy="按插件的推荐方案安装并写入游戏配置。适合绝大多数新服务器。" onClick={() => setForm((current) => ({ ...current, deploymentMode: "guided-install" }))} />
|
||||||
<ModeOption active={form.deploymentMode === "existing-server"} title="接管已有服务器" copy="预检指定目录并接入已有实例;不会把它当作一次新安装。" onClick={() => setForm((current) => ({ ...current, deploymentMode: "existing-server" }))} />
|
<ModeOption active={form.deploymentMode === "existing-server"} title="接管已有服务器" copy="预检指定目录并接入已有实例;不会把它当作一次新安装。" onClick={() => setForm((current) => ({ ...current, deploymentMode: "existing-server" }))} />
|
||||||
<ModeOption active={form.deploymentMode === "custom-command"} title="自定义启动方式" copy="用于非标准启动器或脚本;需由 Run 策略允许。" onClick={() => setForm((current) => ({ ...current, deploymentMode: "custom-command" }))} />
|
<ModeOption active={form.deploymentMode === "custom-command"} title="自定义启动方式" copy="用于非标准启动器或脚本;需由 Run 策略允许。" onClick={() => setForm((current) => ({ ...current, deploymentMode: "custom-command" }))} />
|
||||||
</div></div>}
|
</div></div>}
|
||||||
{step === configurationStep && <div className="deployment-workflow-body">{kind === "edit" && onReveal && <div className="form-guidance"><strong>已读取受保护配置</strong><span>{revealBusy ? "正在读取已保存的目录和命令…" : "这些值只保留在当前编辑窗口,关闭后会清除。"}</span>{revealError && <><span className="field-help">{revealError}</span><button type="button" className="primary-command" disabled={busy || revealBusy} onClick={() => void revealSavedInputs()}>重试读取</button></>}</div>}<div className="form-grid">
|
{step === configurationStep && <div className="deployment-workflow-body">{kind === "edit" && onReveal && <div className="form-guidance"><strong>已读取受保护配置</strong><span>{revealBusy ? "正在读取已保存的目录和命令…" : "这些值只保留在当前编辑窗口,关闭后会清除。"}</span>{revealError && <><span className="field-help">{revealError}</span><button type="button" className="primary-command" disabled={busy || revealBusy} onClick={() => void revealSavedInputs()}>重试读取</button></>}</div>}<div className="form-grid">
|
||||||
{kind === "edit" && <label>部署方式<select name="deploymentMode" value={form.deploymentMode} onChange={updateForm}><option value="guided-install">新建并安装</option><option value="existing-server">接管已有服务器</option><option value="custom-command">自定义启动方式</option></select><small className="field-help">可在此调整部署方式;Run 会按心跳自动识别服务器。</small></label>}
|
{kind === "edit" && <label>部署方式<select name="deploymentMode" value={form.deploymentMode} onChange={updateForm}><option value="guided-install">新建并安装</option><option value="existing-server">接管已有服务器</option><option value="custom-command">自定义启动方式</option></select><small className="field-help">可在此调整部署方式;Run 会按心跳自动识别服务器。</small></label>}
|
||||||
{form.deploymentMode === "guided-install" && <label>安装目录{isScum ? "(必填)" : "(可选)"}<input name="serverRoot" value={form.serverRoot} onChange={updateForm} placeholder={deployment?.serverRootConfigured ? "留空保持已配置安装目录" : "完整绝对路径"} autoComplete="off" required={isScum && !deployment?.serverRootConfigured} /><small className="field-help">新建服务器的安装目标;SCUM 部署模板必须明确安装目录。</small></label>}
|
{form.deploymentMode === "guided-install" && <label>安装目录(可选)<input name="serverRoot" value={form.serverRoot} onChange={updateForm} placeholder={deployment?.serverRootConfigured ? "留空保持已配置安装目录" : "完整绝对路径"} autoComplete="off" /><small className="field-help">新建服务器的安装目标;具体要求由插件生命周期动作决定。</small></label>}
|
||||||
{form.deploymentMode === "existing-server" && <label>已有服务器目录<input name="serverRoot" value={form.serverRoot} onChange={updateForm} placeholder={deployment?.serverRootConfigured ? "留空保持已接管目录" : "完整绝对路径"} autoComplete="off" required={!deployment?.serverRootConfigured} /><small className="field-help">Run 会先预检目录、插件与端口;不会重装或覆盖现有游戏配置。</small></label>}
|
{form.deploymentMode === "existing-server" && <label>已有服务器目录<input name="serverRoot" value={form.serverRoot} onChange={updateForm} placeholder={deployment?.serverRootConfigured ? "留空保持已接管目录" : "完整绝对路径"} autoComplete="off" required={!deployment?.serverRootConfigured} /><small className="field-help">Run 会先预检目录、插件与端口;不会重装或覆盖现有游戏配置。</small></label>}
|
||||||
{form.deploymentMode === "custom-command" && <label>服务器目录<input name="serverRoot" value={form.serverRoot} onChange={updateForm} placeholder={deployment?.serverRootConfigured ? "留空保持已配置目录" : "完整绝对路径"} autoComplete="off" /><small className="field-help">服务器文件、数据与配置的主目录。</small></label>}
|
{form.deploymentMode === "custom-command" && <label>服务器目录<input name="serverRoot" value={form.serverRoot} onChange={updateForm} placeholder={deployment?.serverRootConfigured ? "留空保持已配置目录" : "完整绝对路径"} autoComplete="off" /><small className="field-help">服务器文件、数据与配置的主目录。</small></label>}
|
||||||
{form.deploymentMode === "guided-install" && pluginFields.map((field) => (
|
{form.deploymentMode === "guided-install" && pluginFields.map((field) => (
|
||||||
@@ -129,11 +127,11 @@ export function ServerDeploymentWorkflow({ open, kind, plugins, initialForm, dep
|
|||||||
)}</label>
|
)}</label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{form.deploymentMode === "guided-install" && <GuidedInstallPlan pluginName={pluginLabel(selectedPlugin, form.pluginId)} isScum={isScum} />}
|
{form.deploymentMode === "guided-install" && <GuidedInstallPlan pluginName={pluginLabel(selectedPlugin, form.pluginId)} />}
|
||||||
{form.deploymentMode === "existing-server" && <ExistingServerAdoptionPlan pluginName={pluginLabel(selectedPlugin, form.pluginId)} isScum={isScum} />}
|
{form.deploymentMode === "existing-server" && <ExistingServerAdoptionPlan pluginName={pluginLabel(selectedPlugin, form.pluginId)} />}
|
||||||
{form.deploymentMode === "custom-command" && <details className="provider-advanced-settings" open><summary>高级启动设置</summary><p className="field-help">只有自定义启动器需要这些设置。执行目录留空时,Run 以服务器目录执行。</p><div className="form-grid"><label>启动命令<input name="startCommand" value={form.startCommand} onChange={updateForm} placeholder={deployment?.startCommandConfigured ? "留空保持已配置启动命令" : "必填,例如 ./start-server"} autoComplete="off" required={!deployment?.startCommandConfigured} /></label><label>命令解释器<select name="shell" value={form.shell} onChange={updateForm}><option value="">直接 argv(推荐)</option><option value="posix-sh">POSIX sh</option><option value="powershell">PowerShell</option><option value="cmd">Windows cmd</option></select></label><label>执行目录(可选)<input name="workingDirectory" value={form.workingDirectory} onChange={updateForm} placeholder={deployment?.workingDirectoryConfigured ? "留空保持已配置执行目录" : "默认使用服务器目录"} autoComplete="off" /></label><label>安装命令(可选)<input name="installCommand" value={form.installCommand} onChange={updateForm} autoComplete="off" placeholder="留空保持原值或不使用" /></label><label>停止命令(可选)<input name="stopCommand" value={form.stopCommand} onChange={updateForm} autoComplete="off" /></label><label>状态命令<input name="statusCommand" value={form.statusCommand} onChange={updateForm} autoComplete="off" /></label></div></details>}
|
{form.deploymentMode === "custom-command" && <details className="provider-advanced-settings" open><summary>高级启动设置</summary><p className="field-help">只有自定义启动器需要这些设置。执行目录留空时,Run 以服务器目录执行。</p><div className="form-grid"><label>启动命令<input name="startCommand" value={form.startCommand} onChange={updateForm} placeholder={deployment?.startCommandConfigured ? "留空保持已配置启动命令" : "必填,例如 ./start-server"} autoComplete="off" required={!deployment?.startCommandConfigured} /></label><label>命令解释器<select name="shell" value={form.shell} onChange={updateForm}><option value="">直接 argv(推荐)</option><option value="posix-sh">POSIX sh</option><option value="powershell">PowerShell</option><option value="cmd">Windows cmd</option></select></label><label>执行目录(可选)<input name="workingDirectory" value={form.workingDirectory} onChange={updateForm} placeholder={deployment?.workingDirectoryConfigured ? "留空保持已配置执行目录" : "默认使用服务器目录"} autoComplete="off" /></label><label>安装命令(可选)<input name="installCommand" value={form.installCommand} onChange={updateForm} autoComplete="off" placeholder="留空保持原值或不使用" /></label><label>停止命令(可选)<input name="stopCommand" value={form.stopCommand} onChange={updateForm} autoComplete="off" /></label><label>状态命令<input name="statusCommand" value={form.statusCommand} onChange={updateForm} autoComplete="off" /></label></div></details>}
|
||||||
</div>}
|
</div>}
|
||||||
{step === reviewStep && <div className="deployment-workflow-body"><div className="deployment-review"><div><span>插件类型</span><strong>{pluginLabel(selectedPlugin, form.pluginId)}</strong></div>{kind === "create" && <div><span>服务器名称</span><strong>{form.name.trim() || "未填写"}</strong></div>}<div><span>部署方式</span><strong>{form.deploymentMode === "guided-install" ? "新建并安装" : form.deploymentMode === "existing-server" ? "接管已有服务器" : "自定义启动方式"}</strong></div><div><span>{form.deploymentMode === "guided-install" ? "安装目录" : form.deploymentMode === "existing-server" ? "已有服务器目录" : "服务器目录"}</span><strong>{protectedState(form.serverRoot, Boolean(deployment?.serverRootConfigured))}</strong></div>{form.deploymentMode === "custom-command" && <><div><span>启动命令</span><strong>{protectedState(form.startCommand, Boolean(deployment?.startCommandConfigured))}</strong></div><div><span>执行目录</span><strong>{protectedState(form.workingDirectory, Boolean(deployment?.workingDirectoryConfigured))}</strong></div></>}{form.deploymentMode === "guided-install" && <div><span>游戏配置</span><strong>{Object.keys(form.createInputs).length ? `${Object.keys(form.createInputs).length} 项已准备` : "使用插件默认值"}</strong></div>}{isScum && <div><span>完成条件</span><strong>安装/扫描、映射、验证全部通过</strong></div>}</div><div className="form-guidance"><strong>{kind === "create" ? "本次保存创建向导配置" : "本次只保存部署设置"}</strong><span>{kind === "create" ? "Run 会自动识别并上报心跳;部署执行按这里的方式和启动项进行。" : form.deploymentMode === "existing-server" ? "Run 将自动识别并预检现有目录;不会重装或覆盖已有游戏配置。" : "保存后由平台保留受保护部署设置;路径和命令仅在本次显式展示后可见。"}</span></div></div>}
|
{step === reviewStep && <div className="deployment-workflow-body"><div className="deployment-review"><div><span>插件类型</span><strong>{pluginLabel(selectedPlugin, form.pluginId)}</strong></div>{kind === "create" && <div><span>服务器名称</span><strong>{form.name.trim() || "未填写"}</strong></div>}<div><span>部署方式</span><strong>{form.deploymentMode === "guided-install" ? "新建并安装" : form.deploymentMode === "existing-server" ? "接管已有服务器" : "自定义启动方式"}</strong></div><div><span>{form.deploymentMode === "guided-install" ? "安装目录" : form.deploymentMode === "existing-server" ? "已有服务器目录" : "服务器目录"}</span><strong>{protectedState(form.serverRoot, Boolean(deployment?.serverRootConfigured))}</strong></div>{form.deploymentMode === "custom-command" && <><div><span>启动命令</span><strong>{protectedState(form.startCommand, Boolean(deployment?.startCommandConfigured))}</strong></div><div><span>执行目录</span><strong>{protectedState(form.workingDirectory, Boolean(deployment?.workingDirectoryConfigured))}</strong></div></>}{form.deploymentMode === "guided-install" && <div><span>游戏配置</span><strong>{Object.keys(form.createInputs).length ? `${Object.keys(form.createInputs).length} 项已准备` : "使用插件默认值"}</strong></div>}<div><span>完成条件</span><strong>按插件声明完成预检、执行与验证</strong></div></div><div className="form-guidance"><strong>{kind === "create" ? "本次保存创建向导配置" : "本次只保存部署设置"}</strong><span>{kind === "create" ? "Run 会自动识别并上报心跳;部署执行按这里的方式和启动项进行。" : form.deploymentMode === "existing-server" ? "Run 将自动识别并预检现有目录;不会重装或覆盖已有游戏配置。" : "保存后由平台保留受保护部署设置;路径和命令仅在本次显式展示后可见。"}</span></div></div>}
|
||||||
<div className="confirm-actions"><button type="button" disabled={busy} onClick={() => step === 0 ? closeWorkflow() : setStep((current) => current - 1)}>{step === 0 ? "取消" : "上一步"}</button>{step < reviewStep ? <button type="submit" className="confirm-primary" disabled={busy || !canContinue()}><CircleDashed size={16} /><span>下一步</span></button> : <button type="submit" className="confirm-primary" disabled={busy}><Rocket size={16} /><span>{busy ? "保存中…" : actionLabel}</span></button>}</div>
|
<div className="confirm-actions"><button type="button" disabled={busy} onClick={() => step === 0 ? closeWorkflow() : setStep((current) => current - 1)}>{step === 0 ? "取消" : "上一步"}</button>{step < reviewStep ? <button type="submit" className="confirm-primary" disabled={busy || !canContinue()}><CircleDashed size={16} /><span>下一步</span></button> : <button type="submit" className="confirm-primary" disabled={busy}><Rocket size={16} /><span>{busy ? "保存中…" : actionLabel}</span></button>}</div>
|
||||||
</form>
|
</form>
|
||||||
</ManagementDialog>;
|
</ManagementDialog>;
|
||||||
@@ -141,30 +139,19 @@ export function ServerDeploymentWorkflow({ open, kind, plugins, initialForm, dep
|
|||||||
|
|
||||||
function ModeOption({ active, title, copy, onClick }: { active: boolean; title: string; copy: string; onClick: () => void }) { return <button type="button" className={cx("deployment-mode-option", active && "deployment-mode-option-active")} onClick={onClick}><strong>{title}</strong><span>{copy}</span></button>; }
|
function ModeOption({ active, title, copy, onClick }: { active: boolean; title: string; copy: string; onClick: () => void }) { return <button type="button" className={cx("deployment-mode-option", active && "deployment-mode-option-active")} onClick={onClick}><strong>{title}</strong><span>{copy}</span></button>; }
|
||||||
|
|
||||||
function GuidedInstallPlan({ pluginName, isScum }: { pluginName: string; isScum: boolean }) {
|
function GuidedInstallPlan({ pluginName }: { pluginName: string }) {
|
||||||
const steps = isScum ? [
|
const steps = [
|
||||||
{ icon: ScanSearch, title: "预检目录与端口", copy: "确认安装目录可用、Run 环境兼容且端口可用。" },
|
|
||||||
{ icon: Download, title: "下载 SCUM Server", copy: "通过 SteamCMD 安装 App 3792580 到该目录。" },
|
|
||||||
{ icon: SlidersHorizontal, title: "写入游戏配置", copy: "把本页的名称、端口与人数写入 ServerSettings.ini。" },
|
|
||||||
{ icon: HeartPulse, title: "启动并健康验证", copy: "检查可执行文件、版本、配置、端口和服务进程。" }
|
|
||||||
] : [
|
|
||||||
{ icon: ScanSearch, title: "预检目录与 Run", copy: "确认安装目录、权限、端口与 Run 环境可用。" },
|
{ icon: ScanSearch, title: "预检目录与 Run", copy: "确认安装目录、权限、端口与 Run 环境可用。" },
|
||||||
{ icon: Download, title: "安装游戏服务端", copy: "按插件声明的推荐方案安装到该目录。" },
|
{ icon: Download, title: "安装游戏服务端", copy: "按插件声明的推荐方案安装到该目录。" },
|
||||||
{ icon: SlidersHorizontal, title: "写入游戏配置", copy: "将本页填写的游戏参数交给自动部署流程。" },
|
{ icon: SlidersHorizontal, title: "写入游戏配置", copy: "将本页填写的游戏参数交给自动部署流程。" },
|
||||||
{ icon: HeartPulse, title: "启动并健康验证", copy: "只有启动与插件要求的验证通过才会显示成功。" }
|
{ icon: HeartPulse, title: "启动并健康验证", copy: "只有启动与插件要求的验证通过才会显示成功。" }
|
||||||
];
|
];
|
||||||
|
|
||||||
return <section className="guided-install-plan" aria-label="新建并安装执行流程"><div className="guided-install-plan-heading"><div><strong>确认后,{pluginName} 会这样安装</strong><span>“安装目录”就是游戏服务端、数据和配置将落地的位置;它不是命令执行目录,也不会在日志中回显。</span></div><small>{isScum ? "全部 4 步通过才算安装成功" : "Run 按插件契约执行"}</small></div><ol>{steps.map(({ icon: Icon, title, copy }, index) => <li key={title}><span><Icon size={16} /></span><div><strong>{index + 1}. {title}</strong><small>{copy}</small></div></li>)}</ol><p><strong>不会做:</strong>{isScum ? "不会跳过验证就标记成功;失败时不会暴露你的目录、命令或凭据。" : "不会把受保护的路径、命令或凭据回显给浏览器。"}</p></section>;
|
return <section className="guided-install-plan" aria-label="新建并安装执行流程"><div className="guided-install-plan-heading"><div><strong>确认后,{pluginName} 会这样安装</strong><span>“安装目录”就是游戏服务端、数据和配置将落地的位置;平台普通响应不展示受保护路径,插件日志按插件输出原样显示。</span></div><small>Run 按插件契约执行</small></div><ol>{steps.map(({ icon: Icon, title, copy }, index) => <li key={title}><span><Icon size={16} /></span><div><strong>{index + 1}. {title}</strong><small>{copy}</small></div></li>)}</ol><p><strong>不会做:</strong>不会把受保护的路径、命令或凭据作为平台普通响应回显给浏览器。</p></section>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ExistingServerAdoptionPlan({ pluginName, isScum }: { pluginName: string; isScum: boolean }) {
|
function ExistingServerAdoptionPlan({ pluginName }: { pluginName: string }) {
|
||||||
const steps = isScum ? [
|
const steps = [
|
||||||
{ icon: FolderCog, title: "定位服务端根目录", copy: "填写包含 SCUM 服务端文件、数据与配置的目录,不是 Steam 库或 SteamCMD 目录。" },
|
|
||||||
{ icon: ScanSearch, title: "Run 本机预检", copy: "检查目录权限、可执行文件、版本、Steam App 标记和所需端口。" },
|
|
||||||
{ icon: SlidersHorizontal, title: "只读扫描配置", copy: "识别 ServerSettings.ini 与现有参数;接管不会写入或覆盖它们。" },
|
|
||||||
{ icon: ServerCog, title: "建立生命周期", copy: "Run 自动识别这台实例,后续启动、停止和日志仍走平台通道。" },
|
|
||||||
{ icon: HeartPulse, title: "健康验证", copy: "确认端口、进程与配置可读后,才标记为接管成功。" }
|
|
||||||
] : [
|
|
||||||
{ icon: FolderCog, title: "定位服务端根目录", copy: "填写已有服务端文件、数据与配置所在的主目录。" },
|
{ icon: FolderCog, title: "定位服务端根目录", copy: "填写已有服务端文件、数据与配置所在的主目录。" },
|
||||||
{ icon: ScanSearch, title: "Run 本机预检", copy: "检查目录权限、插件识别和端口是否可用。" },
|
{ icon: ScanSearch, title: "Run 本机预检", copy: "检查目录权限、插件识别和端口是否可用。" },
|
||||||
{ icon: SlidersHorizontal, title: "只读扫描配置", copy: "读取插件需要的现有状态,不把新建默认值写进服务器。" },
|
{ icon: SlidersHorizontal, title: "只读扫描配置", copy: "读取插件需要的现有状态,不把新建默认值写进服务器。" },
|
||||||
@@ -172,5 +159,5 @@ function ExistingServerAdoptionPlan({ pluginName, isScum }: { pluginName: string
|
|||||||
{ icon: HeartPulse, title: "健康验证", copy: "验证通过后才标记为接管成功。" }
|
{ icon: HeartPulse, title: "健康验证", copy: "验证通过后才标记为接管成功。" }
|
||||||
];
|
];
|
||||||
|
|
||||||
return <section className="guided-install-plan" aria-label="接管已有服务器执行流程"><div className="guided-install-plan-heading"><div><strong>确认后,{pluginName} 会这样接管</strong><span>目录只会交给 Run 在本机使用;平台、浏览器和日志都不会显示原始路径。</span></div><small>先扫描,后自动识别</small></div><ol>{steps.map(({ icon: Icon, title, copy }, index) => <li key={title}><span><Icon size={16} /></span><div><strong>{index + 1}. {title}</strong><small>{copy}</small></div></li>)}</ol>{isScum ? <p><strong>SCUM 与 SteamCMD:</strong>接管只需要服务端根目录,不需要填写 SteamCMD 目录。Run 可能按本机策略检查 SteamCMD 是否可用,但它不是接管输入。<br /><strong>升级:</strong>接管不会升级游戏;当前平台尚未提供 SCUM 服务端的自动升级任务,不能承诺自动升级。升级能力需要单独的 SteamCMD 更新任务与备份/健康验证流程。</p> : <p><strong>不会做:</strong>不会重新安装、覆盖已有游戏配置,或把受保护路径回显给浏览器。</p>}</section>;
|
return <section className="guided-install-plan" aria-label="接管已有服务器执行流程"><div className="guided-install-plan-heading"><div><strong>确认后,{pluginName} 会这样接管</strong><span>目录只会交给 Run 在本机使用;平台普通响应不展示原始路径,插件日志按插件输出原样显示。</span></div><small>先扫描,后自动识别</small></div><ol>{steps.map(({ icon: Icon, title, copy }, index) => <li key={title}><span><Icon size={16} /></span><div><strong>{index + 1}. {title}</strong><small>{copy}</small></div></li>)}</ol><p><strong>不会做:</strong>不会重新安装、覆盖已有游戏配置,或把受保护路径作为平台普通响应回显给浏览器。</p></section>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ Plugin page runs with safe platform context.
|
|||||||
- `plugin-lifecycle.request`: declared plugin lifecycle request through Platform.
|
- `plugin-lifecycle.request`: declared plugin lifecycle request through Platform.
|
||||||
- `ai.invoke`: platform-mediated AI invocation.
|
- `ai.invoke`: platform-mediated AI invocation.
|
||||||
|
|
||||||
The host intersects manifest-level and page-level permissions/actions before exposing context. The SCUM operations page additionally intersects its command, snapshot, and query-template keys with `gameClientBridge.pages.operations`; it does not synthesize undeclared SCUM semantics.
|
The host intersects manifest-level and page-level permissions/actions before exposing context. A plugin page additionally intersects its command, snapshot, and query-template keys with the matching `gameClientBridge.pages.*` declaration; it does not synthesize undeclared game semantics.
|
||||||
|
|
||||||
## Forbidden
|
## Forbidden
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ describe("first-party console pages", () => {
|
|||||||
|
|
||||||
expect(runPlatformOptions(plugin, "darwin")).toEqual(["windows", "linux"]);
|
expect(runPlatformOptions(plugin, "darwin")).toEqual(["windows", "linux"]);
|
||||||
expect(runPlatformOptions(undefined, "windows")).toEqual(["windows"]);
|
expect(runPlatformOptions(undefined, "windows")).toEqual(["windows"]);
|
||||||
|
expect(serversPageSource).not.toContain("quickRuntimeDefaultsForPlugin");
|
||||||
|
expect(serversPageSource).not.toContain("pluginId.toLowerCase().includes");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders the platform overview with first-screen health modules", () => {
|
it("renders the platform overview with first-screen health modules", () => {
|
||||||
@@ -184,23 +186,25 @@ describe("first-party console pages", () => {
|
|||||||
expect(serversPageSource).not.toContain('instance.state === "running" || instance.state === "installing"');
|
expect(serversPageSource).not.toContain('instance.state === "running" || instance.state === "installing"');
|
||||||
expect(serverDetailPageSource).not.toContain("<ServerDeploymentWorkflow");
|
expect(serverDetailPageSource).not.toContain("<ServerDeploymentWorkflow");
|
||||||
expect(serverDetailPageSource).toContain("ServerDeploymentSection");
|
expect(serverDetailPageSource).toContain("ServerDeploymentSection");
|
||||||
expect(serverDeploymentWorkflowSource).toContain("基本信息");
|
expect(serverDeploymentWorkflowSource).toContain("基本信息");
|
||||||
expect(serverDeploymentWorkflowSource).toContain("部署方式");
|
expect(serverDeploymentWorkflowSource).toContain("部署方式");
|
||||||
expect(serverDeploymentWorkflowSource).toContain("相关配置");
|
expect(serverDeploymentWorkflowSource).toContain("相关配置");
|
||||||
expect(serverDeploymentWorkflowSource).toContain("自动上报心跳");
|
expect(serverDeploymentWorkflowSource).toContain("自动上报心跳");
|
||||||
expect(serverDeploymentWorkflowSource).toContain("创建服务器");
|
expect(serverDeploymentWorkflowSource).toContain("创建服务器");
|
||||||
expect(serverDeploymentWorkflowSource).toContain("执行目录(可选)");
|
expect(serverDeploymentWorkflowSource).toContain("执行目录(可选)");
|
||||||
expect(serverDeploymentWorkflowSource).toContain("默认使用服务器目录");
|
expect(serverDeploymentWorkflowSource).toContain("默认使用服务器目录");
|
||||||
expect(serverDeploymentWorkflowSource).toContain("安装目录{isScum ? \"(必填)\" : \"(可选)\"}");
|
expect(serverDeploymentWorkflowSource).toContain("安装目录(可选)");
|
||||||
expect(serverDeploymentWorkflowSource).toContain("新建并安装执行流程");
|
expect(serverDeploymentWorkflowSource).toContain("新建并安装执行流程");
|
||||||
expect(serverDeploymentWorkflowSource).toContain("安装目录”就是游戏服务端、数据和配置将落地的位置");
|
expect(serverDeploymentWorkflowSource).toContain("安装目录”就是游戏服务端、数据和配置将落地的位置");
|
||||||
expect(serverDeploymentWorkflowSource).toContain("通过 SteamCMD 安装 App 3792580 到该目录");
|
expect(serverDeploymentWorkflowSource).toContain("插件日志按插件输出原样显示");
|
||||||
expect(serverDeploymentWorkflowSource).toContain("全部 4 步通过才算安装成功");
|
expect(serverDeploymentWorkflowSource).toContain("按插件声明的推荐方案安装到该目录");
|
||||||
|
expect(serverDeploymentWorkflowSource).not.toContain("SteamCMD 安装 App 3792580");
|
||||||
|
expect(serverDeploymentWorkflowSource).not.toContain("selectedPlugin?.id === \"game.scum\"");
|
||||||
expect(serverDeploymentWorkflowSource).toContain("已有服务器目录");
|
expect(serverDeploymentWorkflowSource).toContain("已有服务器目录");
|
||||||
expect(serverDeploymentWorkflowSource).toContain("不会重装或覆盖现有游戏配置");
|
expect(serverDeploymentWorkflowSource).toContain("不会重装或覆盖现有游戏配置");
|
||||||
expect(serverDeploymentWorkflowSource).toContain("接管已有服务器执行流程");
|
expect(serverDeploymentWorkflowSource).toContain("接管已有服务器执行流程");
|
||||||
expect(serverDeploymentWorkflowSource).toContain("不需要填写 SteamCMD 目录");
|
expect(serverDeploymentWorkflowSource).not.toContain("不需要填写 SteamCMD 目录");
|
||||||
expect(serverDeploymentWorkflowSource).toContain("当前平台尚未提供 SCUM 服务端的自动升级任务");
|
expect(serverDeploymentWorkflowSource).not.toContain("当前平台尚未提供 SCUM 服务端的自动升级任务");
|
||||||
expect(serverDeploymentWorkflowSource).toContain("Run 会按心跳自动识别服务器");
|
expect(serverDeploymentWorkflowSource).toContain("Run 会按心跳自动识别服务器");
|
||||||
expect(serversPageSource).toContain('onNavigate("serverDetail", { serverId: result.instance.id })');
|
expect(serversPageSource).toContain('onNavigate("serverDetail", { serverId: result.instance.id })');
|
||||||
expect(serverDetailPageSource).not.toContain("运行配置绑定");
|
expect(serverDetailPageSource).not.toContain("运行配置绑定");
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ describe("PluginsPage", () => {
|
|||||||
expect(html).toContain("停用");
|
expect(html).toContain("停用");
|
||||||
expect(html).toContain("SCUM Simple RCON UE4SS DLL");
|
expect(html).toContain("SCUM Simple RCON UE4SS DLL");
|
||||||
expect(html).toContain("启动前自动校验和更新");
|
expect(html).toContain("启动前自动校验和更新");
|
||||||
expect(html).toContain("运行:SCUM 服务器受监管启动");
|
expect(html).toContain("运行:目标服务端受监管启动");
|
||||||
expect(html).toContain("Linux 启动前拒绝");
|
expect(html).toContain("Linux 启动前拒绝");
|
||||||
expect(html).toContain('role="dialog"');
|
expect(html).toContain('role="dialog"');
|
||||||
expect(html).not.toContain("billing");
|
expect(html).not.toContain("billing");
|
||||||
|
|||||||
@@ -138,6 +138,10 @@ describe("ServerDetailPage config write approval", () => {
|
|||||||
expect(serverDetailPageSource).not.toContain("tcp://");
|
expect(serverDetailPageSource).not.toContain("tcp://");
|
||||||
expect(serverDetailPageSource).not.toContain("mysql://");
|
expect(serverDetailPageSource).not.toContain("mysql://");
|
||||||
expect(serverDetailPageSource).not.toContain("sqlite://");
|
expect(serverDetailPageSource).not.toContain("sqlite://");
|
||||||
|
expect(serverDetailPageSource).not.toContain("SCUM 部署模板");
|
||||||
|
expect(serverDetailPageSource).not.toContain("isScumTemplate");
|
||||||
|
expect(serverDetailPageSource).toContain("部署验证模板");
|
||||||
|
expect(serverDetailPageSource).toContain("平台普通响应不会回显");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not expose manual runtime configuration surfaces", () => {
|
it("does not expose manual runtime configuration surfaces", () => {
|
||||||
|
|||||||
@@ -379,35 +379,35 @@ function ServerMetadataSection({ instance, session, operations, onChanged }: Ser
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ServerDeploymentSectionProps {
|
interface ServerDeploymentSectionProps {
|
||||||
instance: ServerInstanceResponse;
|
instance: ServerInstanceResponse;
|
||||||
deployment: LoadState<ServerDeploymentResponse>;
|
deployment: LoadState<ServerDeploymentResponse>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ServerDeploymentSection({ instance, deployment }: ServerDeploymentSectionProps) {
|
function ServerDeploymentSection({ instance, deployment }: ServerDeploymentSectionProps) {
|
||||||
if (deployment.status === "loading") return <LoadingState label="正在加载部署定义…" compact />;
|
if (deployment.status === "loading") return <LoadingState label="正在加载部署定义…" compact />;
|
||||||
if (deployment.status === "error") return <ErrorState title="部署定义不可用" reason={deployment.reason} diagnosticId={`deployment:${instance.id}`} compact />;
|
if (deployment.status === "error") return <ErrorState title="部署定义不可用" reason={deployment.reason} diagnosticId={`deployment:${instance.id}`} compact />;
|
||||||
const view = deployment.data;
|
const view = deployment.data;
|
||||||
const projection = view.projection;
|
const projection = view.projection;
|
||||||
const isScumTemplate = (instance.pluginId === "game.scum" && (view.mode === "guided-install" || view.mode === "existing-server")) || projection?.templateKey?.startsWith("scum-");
|
const hasProjection = Boolean(projection?.templateKey || projection?.templateVersion || projection?.preflightState || projection?.discoveryState || projection?.mappingState || projection?.verificationState || projection?.failureCode);
|
||||||
return <article className="console-panel" aria-label="server deployment">
|
return <article className="console-panel" aria-label="server deployment">
|
||||||
<div className="panel-header"><h2><PackageOpen size={16} style={{ verticalAlign: "-2px" }} /> 部署定义</h2><span className="page-status">{view.mode || "未配置"} · 修订 {view.revision}</span></div>
|
<div className="panel-header"><h2><PackageOpen size={16} style={{ verticalAlign: "-2px" }} /> 部署定义</h2><span className="page-status">{view.mode || "未配置"} · 修订 {view.revision}</span></div>
|
||||||
<p className="section-copy">服务器目录是主目录;执行目录只用于高级自定义启动,留空时继承服务器目录。路径和命令均为受保护输入,不会回显。</p>
|
<p className="section-copy">服务器目录是主目录;执行目录只用于高级自定义启动,留空时继承服务器目录。路径和命令均为受保护输入,平台普通响应不会回显。</p>
|
||||||
<div className="console-row-list"><div className="console-row"><span>服务器目录</span><strong>{view.serverRootConfigured ? "已配置" : "未配置"}</strong></div><div className="console-row"><span>高级执行目录</span><strong>{view.workingDirectoryConfigured ? "已配置" : "使用服务器目录"}</strong></div><div className="console-row"><span>启动设置</span><strong>{view.startCommandConfigured ? "已配置" : view.mode === "custom-command" ? "未配置" : "插件引导"}</strong></div>{view.latestDispatch && <div className="console-row"><span>最近 Run 调度</span><strong>{view.latestDispatch.deploymentDefinitionIncluded ? `部署定义已随任务发送 · r${view.latestDispatch.deploymentRevision} · ${view.latestDispatch.jobState}` : "未携带部署定义"}</strong></div>}{view.latestDispatch?.runConfirmed && <div className="console-row"><span>Run 执行确认</span><strong>已按 r{view.latestDispatch.deploymentRevision} 确认执行</strong></div>}</div>
|
<div className="console-row-list"><div className="console-row"><span>服务器目录</span><strong>{view.serverRootConfigured ? "已配置" : "未配置"}</strong></div><div className="console-row"><span>高级执行目录</span><strong>{view.workingDirectoryConfigured ? "已配置" : "使用服务器目录"}</strong></div><div className="console-row"><span>启动设置</span><strong>{view.startCommandConfigured ? "已配置" : view.mode === "custom-command" ? "未配置" : "插件引导"}</strong></div>{view.latestDispatch && <div className="console-row"><span>最近 Run 调度</span><strong>{view.latestDispatch.deploymentDefinitionIncluded ? `部署定义已随任务发送 · r${view.latestDispatch.deploymentRevision} · ${view.latestDispatch.jobState}` : "未携带部署定义"}</strong></div>}{view.latestDispatch?.runConfirmed && <div className="console-row"><span>Run 执行确认</span><strong>已按 r{view.latestDispatch.deploymentRevision} 确认执行</strong></div>}</div>
|
||||||
{isScumTemplate && <div className="console-row-list" style={{ marginTop: 12 }}><div className="console-row"><span>SCUM 部署模板</span><strong>{projection?.templateVersion ? `${projection.templateKey ?? "已选择"} · v${projection.templateVersion}` : "等待 Run 预检"}</strong></div><div className="console-row"><span>预检 / 扫描</span><strong>{deploymentProjectionLabel(projection?.preflightState)} / {deploymentProjectionLabel(projection?.discoveryState)}</strong></div><div className="console-row"><span>配置映射 / 健康验证</span><strong>{deploymentProjectionLabel(projection?.mappingState)} / {deploymentProjectionLabel(projection?.verificationState)}</strong></div>{projection?.failureCode && <div className="console-row"><span>失败原因</span><strong>{projection.failureCode}</strong></div>}</div>}
|
{hasProjection && <div className="console-row-list" style={{ marginTop: 12 }}><div className="console-row"><span>部署验证模板</span><strong>{projection?.templateVersion ? `${projection.templateKey ?? "已选择"} · v${projection.templateVersion}` : projection?.templateKey ?? "等待 Run 预检"}</strong></div><div className="console-row"><span>预检 / 扫描</span><strong>{deploymentProjectionLabel(projection?.preflightState)} / {deploymentProjectionLabel(projection?.discoveryState)}</strong></div><div className="console-row"><span>配置映射 / 健康验证</span><strong>{deploymentProjectionLabel(projection?.mappingState)} / {deploymentProjectionLabel(projection?.verificationState)}</strong></div>{projection?.failureCode && <div className="console-row"><span>失败原因</span><strong>{projection.failureCode}</strong></div>}</div>}
|
||||||
</article>;
|
</article>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function deploymentProjectionLabel(value?: string): string {
|
function deploymentProjectionLabel(value?: string): string {
|
||||||
switch (value) {
|
switch (value) {
|
||||||
case "queued": return "排队中";
|
case "queued": return "排队中";
|
||||||
case "running": return "执行中";
|
case "running": return "执行中";
|
||||||
case "passed": return "已通过";
|
case "passed": return "已通过";
|
||||||
case "applied": return "已写入";
|
case "applied": return "已写入";
|
||||||
case "unchanged": return "未变化";
|
case "unchanged": return "未变化";
|
||||||
case "failed": return "失败";
|
case "failed": return "失败";
|
||||||
case "skipped": return "已跳过";
|
case "skipped": return "已跳过";
|
||||||
default: return "待返回";
|
default: return "待返回";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function deploymentProgressLabel(progress: JobResponse["progress"]): string {
|
function deploymentProgressLabel(progress: JobResponse["progress"]): string {
|
||||||
|
|||||||
@@ -3,7 +3,16 @@ import { type CSSProperties, type FormEvent, useCallback, useEffect, useMemo, us
|
|||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
|
|
||||||
import { platformApiClient } from "../api/client";
|
import { platformApiClient } from "../api/client";
|
||||||
import type { GamePluginResponse, JobResponse, RunEndpointResponse, ServerInstanceResponse, ServerMetricsResponse } from "../api/types";
|
import type {
|
||||||
|
DependencyCatalogResponse,
|
||||||
|
DependencyPlanViewResponse,
|
||||||
|
DependencyProbeViewResponse,
|
||||||
|
GamePluginResponse,
|
||||||
|
JobResponse,
|
||||||
|
RunEndpointResponse,
|
||||||
|
ServerInstanceResponse,
|
||||||
|
ServerMetricsResponse
|
||||||
|
} from "../api/types";
|
||||||
import {
|
import {
|
||||||
RuntimeTaskProgressDialog,
|
RuntimeTaskProgressDialog,
|
||||||
type RuntimeTaskDialogAction,
|
type RuntimeTaskDialogAction,
|
||||||
@@ -21,8 +30,8 @@ import type { PageComponentProps } from "../contracts/page";
|
|||||||
import {
|
import {
|
||||||
canDeleteServer,
|
canDeleteServer,
|
||||||
defaultServerCreateForm,
|
defaultServerCreateForm,
|
||||||
pluginCreateInputDefaults,
|
pluginCreateInputDefaults,
|
||||||
runtimeObservationFreshness,
|
runtimeObservationFreshness,
|
||||||
type ServerCreateFormState
|
type ServerCreateFormState
|
||||||
} from "../contracts/serverManagement";
|
} from "../contracts/serverManagement";
|
||||||
import { summarizeServerOperations } from "../contracts/operationsConsole";
|
import { summarizeServerOperations } from "../contracts/operationsConsole";
|
||||||
@@ -223,11 +232,11 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function openRunTargetSelection(instance: ServerInstanceResponse) {
|
function openRunTargetSelection(instance: ServerInstanceResponse) {
|
||||||
const defaults = quickRuntimeDefaultsForPlugin(instance.pluginId);
|
const plugin = plugins.find((item) => item.id === instance.pluginId);
|
||||||
|
const targetOs = runPlatformOptions(plugin, "linux")[0] ?? "linux";
|
||||||
setRuntimeTaskActions([]);
|
setRuntimeTaskActions([]);
|
||||||
setRunTargetSelection({ instance, targetOs: defaults.runOs, targetArch: "amd64" });
|
setRunTargetSelection({ instance, targetOs, targetArch: "amd64" });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRunTargetSubmit(event: FormEvent<HTMLFormElement>) {
|
async function handleRunTargetSubmit(event: FormEvent<HTMLFormElement>) {
|
||||||
@@ -292,7 +301,6 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const defaults = quickRuntimeDefaultsForPlugin(instance.pluginId);
|
|
||||||
const intent = quickRuntimeActionLabel(action);
|
const intent = quickRuntimeActionLabel(action);
|
||||||
const operationId = operations.begin({ intent, targetKind: "server", targetId: `${instance.id}:${action}`, requester: session.displayName });
|
const operationId = operations.begin({ intent, targetKind: "server", targetId: `${instance.id}:${action}`, requester: session.displayName });
|
||||||
setRuntimeTaskActions([]);
|
setRuntimeTaskActions([]);
|
||||||
@@ -300,40 +308,40 @@ export function ServersPage({ session, operations, onNavigate }: PageComponentPr
|
|||||||
await requireQuickRuntimeActionAvailable(instance.id, action);
|
await requireQuickRuntimeActionAvailable(instance.id, action);
|
||||||
let message: string;
|
let message: string;
|
||||||
message = await runtimeTask.runTask({
|
message = await runtimeTask.runTask({
|
||||||
title: intent,
|
title: intent,
|
||||||
description: quickRuntimeTaskDescription(instance, action),
|
description: quickRuntimeTaskDescription(instance, action),
|
||||||
stages: quickRuntimeStages(action),
|
stages: quickRuntimeStages(action),
|
||||||
executeStageIndex: quickRuntimeExecuteStageIndex(action),
|
executeStageIndex: quickRuntimeExecuteStageIndex(action),
|
||||||
execute: async () => {
|
execute: async () => {
|
||||||
if (action === "download-run") {
|
if (action === "download-run") {
|
||||||
const reference = await platformApiClient.downloadLatestRunDistribution(instance.id);
|
const reference = await platformApiClient.downloadLatestRunDistribution(instance.id);
|
||||||
await downloadArtifactReference(reference, (artifactId) => platformApiClient.downloadArtifactContent(artifactId));
|
await downloadArtifactReference(reference, (artifactId) => platformApiClient.downloadArtifactContent(artifactId));
|
||||||
return `run 下载已开始,artifact ${reference.artifactId},文件 ${safeArtifactFilename(reference.filename)}`;
|
return `run 下载已开始,artifact ${reference.artifactId},文件 ${safeArtifactFilename(reference.filename)}`;
|
||||||
}
|
}
|
||||||
if (action === "push-run-update") {
|
if (action === "push-run-update") {
|
||||||
const reference = await platformApiClient.downloadLatestRunDistribution(instance.id);
|
const reference = await platformApiClient.downloadLatestRunDistribution(instance.id);
|
||||||
const update = await platformApiClient.pushRunUpdate(instance.id, runUpdateRequest(instance.id, reference.artifactId, reference.checksum));
|
const update = await platformApiClient.pushRunUpdate(instance.id, runUpdateRequest(instance.id, reference.artifactId, reference.checksum));
|
||||||
return `run 更新任务已排队,job ${update.jobId ?? update.id}`;
|
return `run 更新任务已排队,job ${update.jobId ?? update.id}`;
|
||||||
}
|
}
|
||||||
if (action === "reset-run-key") {
|
if (action === "reset-run-key") {
|
||||||
const key = await platformApiClient.resetRunKey(instance.id);
|
const key = await platformApiClient.resetRunKey(instance.id);
|
||||||
return `run 密钥已重置到第 ${key.generation} 代,旧 run 会话已失效,请重新生成并部署 run`;
|
return `run 密钥已重置到第 ${key.generation} 代,旧 run 会话已失效,请重新生成并部署 run`;
|
||||||
}
|
}
|
||||||
if (action === "dependencies-check") {
|
if (action === "dependencies-check") {
|
||||||
const job = await platformApiClient.checkDependencies(instance.id, dependencyJobRequest(instance.id, defaults.probeKey));
|
|
||||||
return `依赖检查任务已排队,job ${job.id}`;
|
|
||||||
}
|
|
||||||
if (action === "dependencies-install") {
|
|
||||||
const catalog = await platformApiClient.getDependencyCatalog(instance.id);
|
const catalog = await platformApiClient.getDependencyCatalog(instance.id);
|
||||||
const plan = catalog.plans.find((candidate) => candidate.key === defaults.installPlanKey);
|
const probe = firstDependencyProbe(catalog);
|
||||||
const probe = catalog.probes.find((candidate) => candidate.key === defaults.probeKey);
|
const job = await platformApiClient.checkDependencies(instance.id, dependencyJobRequest(instance.id, probe.key));
|
||||||
if (!plan || probe?.installPlanKey !== plan.key) throw new Error("Platform 未返回与当前 probe 匹配的审核安装计划");
|
return `依赖检查任务已排队,job ${job.id}`;
|
||||||
|
}
|
||||||
|
if (action === "dependencies-install") {
|
||||||
|
const catalog = await platformApiClient.getDependencyCatalog(instance.id);
|
||||||
|
const { probe, plan } = firstInstallableDependency(catalog);
|
||||||
const job = await platformApiClient.installDependencies(instance.id, dependencyJobRequest(instance.id, probe.key, plan.key, plan.digest));
|
const job = await platformApiClient.installDependencies(instance.id, dependencyJobRequest(instance.id, probe.key, plan.key, plan.digest));
|
||||||
return `依赖安装任务已排队,job ${job.id}`;
|
return `依赖安装任务已排队,job ${job.id}`;
|
||||||
}
|
|
||||||
throw new Error("该运行操作已下线");
|
|
||||||
}
|
}
|
||||||
});
|
throw new Error("该运行操作已下线");
|
||||||
|
}
|
||||||
|
});
|
||||||
operations.succeed(operationId, message);
|
operations.succeed(operationId, message);
|
||||||
runtimeTask.succeedTask(message);
|
runtimeTask.succeedTask(message);
|
||||||
await refresh();
|
await refresh();
|
||||||
@@ -905,15 +913,6 @@ function quickRuntimeTaskDescription(instance: ServerInstanceResponse, action: S
|
|||||||
return `${instance.name}(${instance.id})${label},通过平台 API 派发并保留可追踪进度。`;
|
return `${instance.name}(${instance.id})${label},通过平台 API 派发并保留可追踪进度。`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function quickRuntimeDefaultsForPlugin(pluginId: string) {
|
|
||||||
const isScum = pluginId.toLowerCase().includes("scum");
|
|
||||||
return {
|
|
||||||
runOs: isScum ? "windows" : "linux",
|
|
||||||
probeKey: isScum ? "steamcmd" : "java-21",
|
|
||||||
installPlanKey: isScum ? "install-steamcmd-linux" : "install-java-linux"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function runPlatformOptions(plugin: GamePluginResponse | undefined, fallback: string): string[] {
|
export function runPlatformOptions(plugin: GamePluginResponse | undefined, fallback: string): string[] {
|
||||||
const options = new Set<string>();
|
const options = new Set<string>();
|
||||||
const add = (value: string | undefined) => {
|
const add = (value: string | undefined) => {
|
||||||
@@ -932,6 +931,21 @@ export function runPlatformOptions(plugin: GamePluginResponse | undefined, fallb
|
|||||||
return [...options];
|
return [...options];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function firstDependencyProbe(catalog: DependencyCatalogResponse): DependencyProbeViewResponse {
|
||||||
|
const probe = catalog.probes.find((candidate) => candidate.required) ?? catalog.probes[0];
|
||||||
|
if (!probe) throw new Error("插件未声明可检查的运行依赖");
|
||||||
|
return probe;
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstInstallableDependency(catalog: DependencyCatalogResponse): { probe: DependencyProbeViewResponse; plan: DependencyPlanViewResponse } {
|
||||||
|
for (const probe of catalog.probes) {
|
||||||
|
if (!probe.installPlanKey) continue;
|
||||||
|
const plan = catalog.plans.find((candidate) => candidate.key === probe.installPlanKey);
|
||||||
|
if (plan) return { probe, plan };
|
||||||
|
}
|
||||||
|
throw new Error("插件未声明可安装的运行依赖计划");
|
||||||
|
}
|
||||||
|
|
||||||
function runPlatformLabel(platform: string): string {
|
function runPlatformLabel(platform: string): string {
|
||||||
switch (platform) {
|
switch (platform) {
|
||||||
case "linux":
|
case "linux":
|
||||||
|
|||||||
@@ -99,8 +99,3 @@ function copyStreamChunk(value: Uint8Array): ArrayBuffer {
|
|||||||
copy.set(value);
|
copy.set(value);
|
||||||
return copy.buffer;
|
return copy.buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function safeArtifactError(error: unknown): string {
|
|
||||||
const message = error instanceof Error ? error.message : "制品传输失败";
|
|
||||||
return message.replace(/\/Users\/[^\s]+/g, "[path]").replace(/Bearer\s+[^\s]+/gi, "[token]").replace(/sk-[A-Za-z0-9_-]+/g, "[secret]");
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1243,21 +1243,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"installPlans": [
|
"installPlans": [
|
||||||
{
|
|
||||||
"key": "install-scum-server",
|
|
||||||
"title": "Install SCUM Dedicated Server",
|
|
||||||
"platforms": [
|
|
||||||
"windows"
|
|
||||||
],
|
|
||||||
"steps": [
|
|
||||||
{
|
|
||||||
"type": "steamcmd-app",
|
|
||||||
"targetKey": "server/install-root",
|
|
||||||
"packageManager": "steamcmd",
|
|
||||||
"packageName": "3792580"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"key": "install-steamcmd-linux",
|
"key": "install-steamcmd-linux",
|
||||||
"title": "Install SteamCMD",
|
"title": "Install SteamCMD",
|
||||||
@@ -1416,7 +1401,7 @@
|
|||||||
"targetKey": "ue4ss/scum-simple-rcon",
|
"targetKey": "ue4ss/scum-simple-rcon",
|
||||||
"modKey": "scum_simple_rcon",
|
"modKey": "scum_simple_rcon",
|
||||||
"dllRef": "ue4ss/Mods/scum_simple_rcon/dlls/main.dll",
|
"dllRef": "ue4ss/Mods/scum_simple_rcon/dlls/main.dll",
|
||||||
"scumExecutableChecksum": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
"targetExecutableChecksum": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||||
"ue4ssAbi": "ue4ss-3.0",
|
"ue4ssAbi": "ue4ss-3.0",
|
||||||
"supportedTargets": [
|
"supportedTargets": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -109,12 +109,6 @@
|
|||||||
"items": { "$ref": "#/$defs/runtimeInstallPlan" },
|
"items": { "$ref": "#/$defs/runtimeInstallPlan" },
|
||||||
"uniqueItems": true
|
"uniqueItems": true
|
||||||
},
|
},
|
||||||
"serverDeployments": {
|
|
||||||
"type": "array",
|
|
||||||
"items": { "$ref": "#/$defs/runtimeServerDeploymentProfile" },
|
|
||||||
"uniqueItems": true,
|
|
||||||
"maxItems": 8
|
|
||||||
},
|
|
||||||
"logSources": {
|
"logSources": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": { "$ref": "#/$defs/runtimeLogSource" },
|
"items": { "$ref": "#/$defs/runtimeLogSource" },
|
||||||
@@ -557,9 +551,9 @@
|
|||||||
"required": ["type", "targetKey"],
|
"required": ["type", "targetKey"],
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
"type": { "enum": ["package", "verified-download", "steamcmd-app", "manual"] },
|
"type": { "enum": ["package", "verified-download", "manual"] },
|
||||||
"targetKey": { "$ref": "#/$defs/logicalKey" },
|
"targetKey": { "$ref": "#/$defs/logicalKey" },
|
||||||
"packageManager": { "enum": ["winget", "choco", "scoop", "apt", "yum", "dnf", "pacman", "zypper", "brew", "steamcmd", "manual"] },
|
"packageManager": { "enum": ["winget", "choco", "scoop", "apt", "yum", "dnf", "pacman", "zypper", "brew"] },
|
||||||
"packageName": { "type": "string", "pattern": "^[a-zA-Z0-9_.:+@/-]+$", "maxLength": 120 },
|
"packageName": { "type": "string", "pattern": "^[a-zA-Z0-9_.:+@/-]+$", "maxLength": 120 },
|
||||||
"version": { "type": "string", "maxLength": 80 },
|
"version": { "type": "string", "maxLength": 80 },
|
||||||
"downloadRef": { "type": "string", "pattern": "^https://[a-zA-Z0-9._~:/?#\\[\\]@!$&'()*+,;=%-]+$", "maxLength": 240 },
|
"downloadRef": { "type": "string", "pattern": "^https://[a-zA-Z0-9._~:/?#\\[\\]@!$&'()*+,;=%-]+$", "maxLength": 240 },
|
||||||
@@ -576,16 +570,6 @@
|
|||||||
{
|
{
|
||||||
"if": { "properties": { "type": { "const": "verified-download" } }, "required": ["type"] },
|
"if": { "properties": { "type": { "const": "verified-download" } }, "required": ["type"] },
|
||||||
"then": { "required": ["downloadRef", "checksum"] }
|
"then": { "required": ["downloadRef", "checksum"] }
|
||||||
},
|
|
||||||
{
|
|
||||||
"if": { "properties": { "type": { "const": "steamcmd-app" } }, "required": ["type"] },
|
|
||||||
"then": {
|
|
||||||
"required": ["packageName"],
|
|
||||||
"properties": {
|
|
||||||
"packageManager": { "const": "steamcmd" },
|
|
||||||
"packageName": { "type": "string", "pattern": "^[0-9]{1,12}$" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -600,68 +584,6 @@
|
|||||||
"steps": { "type": "array", "items": { "$ref": "#/$defs/runtimeInstallStep" }, "minItems": 1, "maxItems": 64 }
|
"steps": { "type": "array", "items": { "$ref": "#/$defs/runtimeInstallStep" }, "minItems": 1, "maxItems": 64 }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"runtimeServerConfigMapping": {
|
|
||||||
"type": "object",
|
|
||||||
"required": ["fieldKey", "configKey", "valueType"],
|
|
||||||
"additionalProperties": false,
|
|
||||||
"properties": {
|
|
||||||
"fieldKey": { "type": "string", "pattern": "^[A-Za-z][A-Za-z0-9._/-]*$", "maxLength": 80 },
|
|
||||||
"configKey": { "$ref": "#/$defs/logicalKey" },
|
|
||||||
"valueType": { "enum": ["text", "integer", "number", "boolean", "port"] },
|
|
||||||
"required": { "type": "boolean" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"runtimeServerDiscoveryMarker": {
|
|
||||||
"type": "object",
|
|
||||||
"required": ["key", "kind", "targetKey"],
|
|
||||||
"additionalProperties": false,
|
|
||||||
"properties": {
|
|
||||||
"key": { "$ref": "#/$defs/logicalKey" },
|
|
||||||
"kind": { "enum": ["file.exists", "command.version", "port.open", "steam.app"] },
|
|
||||||
"targetKey": { "$ref": "#/$defs/logicalKey" },
|
|
||||||
"expected": { "type": "string", "maxLength": 120 },
|
|
||||||
"required": { "type": "boolean" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"runtimeServerVerificationCheck": {
|
|
||||||
"type": "object",
|
|
||||||
"required": ["key", "kind", "targetKey"],
|
|
||||||
"additionalProperties": false,
|
|
||||||
"properties": {
|
|
||||||
"key": { "$ref": "#/$defs/logicalKey" },
|
|
||||||
"kind": { "enum": ["executable.present", "version.matches", "port.bound", "config.readable", "process.healthy"] },
|
|
||||||
"targetKey": { "$ref": "#/$defs/logicalKey" },
|
|
||||||
"required": { "type": "boolean" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"runtimeServerDeploymentProfile": {
|
|
||||||
"type": "object",
|
|
||||||
"required": ["key", "version", "supportedTargets", "steamAppId", "executableKey", "installRootKey", "configKey", "configFormat", "configMappings", "discoveryMarkers", "verificationChecks"],
|
|
||||||
"additionalProperties": false,
|
|
||||||
"properties": {
|
|
||||||
"key": { "$ref": "#/$defs/logicalKey" },
|
|
||||||
"version": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+(?:-[0-9A-Za-z.-]+)?$" },
|
|
||||||
"supportedTargets": { "type": "array", "items": { "$ref": "#/$defs/runtimeTarget" }, "minItems": 1, "uniqueItems": true },
|
|
||||||
"steamAppId": { "type": "string", "pattern": "^[0-9]{1,12}$" },
|
|
||||||
"executableKey": { "$ref": "#/$defs/logicalKey" },
|
|
||||||
"installRootKey": { "$ref": "#/$defs/logicalKey" },
|
|
||||||
"configKey": { "$ref": "#/$defs/logicalKey" },
|
|
||||||
"configFormat": { "enum": ["ini", "json", "yaml", "properties"] },
|
|
||||||
"prerequisites": { "type": "array", "items": { "$ref": "#/$defs/runtimeServerPrerequisite" }, "maxItems": 16 },
|
|
||||||
"configMappings": { "type": "array", "items": { "$ref": "#/$defs/runtimeServerConfigMapping" }, "minItems": 1, "maxItems": 32, "uniqueItems": true },
|
|
||||||
"discoveryMarkers": { "type": "array", "items": { "$ref": "#/$defs/runtimeServerDiscoveryMarker" }, "minItems": 1, "maxItems": 32, "uniqueItems": true },
|
|
||||||
"verificationChecks": { "type": "array", "items": { "$ref": "#/$defs/runtimeServerVerificationCheck" }, "minItems": 4, "maxItems": 16, "uniqueItems": true }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"runtimeServerPrerequisite": {
|
|
||||||
"type": "object",
|
|
||||||
"required": ["key", "kind"],
|
|
||||||
"additionalProperties": false,
|
|
||||||
"properties": {
|
|
||||||
"key": { "$ref": "#/$defs/logicalKey" },
|
|
||||||
"kind": { "enum": ["steamcmd", "windows-vcredist", "windows-directx"] }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"runtimeLogSource": {
|
"runtimeLogSource": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["key", "kind", "streamKey"],
|
"required": ["key", "kind", "streamKey"],
|
||||||
@@ -724,7 +646,7 @@
|
|||||||
"targetKey": { "$ref": "#/$defs/logicalKey" },
|
"targetKey": { "$ref": "#/$defs/logicalKey" },
|
||||||
"modKey": { "type": "string", "pattern": "^[a-z0-9][a-z0-9_-]{0,79}$" },
|
"modKey": { "type": "string", "pattern": "^[a-z0-9][a-z0-9_-]{0,79}$" },
|
||||||
"dllRef": { "type": "string", "pattern": "^ue4ss/Mods/[a-z0-9][a-z0-9_-]{0,79}/dlls/main\\.dll$", "maxLength": 160 },
|
"dllRef": { "type": "string", "pattern": "^ue4ss/Mods/[a-z0-9][a-z0-9_-]{0,79}/dlls/main\\.dll$", "maxLength": 160 },
|
||||||
"scumExecutableChecksum": { "type": "string", "pattern": "^sha256:[a-fA-F0-9]{64}$" },
|
"targetExecutableChecksum": { "type": "string", "pattern": "^sha256:[a-fA-F0-9]{64}$" },
|
||||||
"ue4ssAbi": { "type": "string", "pattern": "^[A-Za-z0-9._-]{1,80}$" },
|
"ue4ssAbi": { "type": "string", "pattern": "^[A-Za-z0-9._-]{1,80}$" },
|
||||||
"supportedTargets": { "type": "array", "items": { "$ref": "#/$defs/runtimeTarget" }, "minItems": 1, "maxItems": 1, "uniqueItems": true },
|
"supportedTargets": { "type": "array", "items": { "$ref": "#/$defs/runtimeTarget" }, "minItems": 1, "maxItems": 1, "uniqueItems": true },
|
||||||
"updateOnStart": { "const": true },
|
"updateOnStart": { "const": true },
|
||||||
@@ -733,7 +655,7 @@
|
|||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
"if": { "properties": { "releaseState": { "const": "ready" } }, "required": ["releaseState"] },
|
"if": { "properties": { "releaseState": { "const": "ready" } }, "required": ["releaseState"] },
|
||||||
"then": { "required": ["releaseUrl", "checksum", "sizeBytes", "scumExecutableChecksum", "ue4ssAbi"] }
|
"then": { "required": ["releaseUrl", "checksum", "sizeBytes", "targetExecutableChecksum", "ue4ssAbi"] }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -320,26 +320,9 @@ function validateDependencyPlans(manifest: unknown): string[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function validateServerDeploymentProfiles(manifest: unknown): string[] {
|
function validateServerDeploymentProfiles(manifest: unknown): string[] {
|
||||||
if (typeof manifest !== "object" || manifest === null || !("server" in manifest)) return [];
|
if (typeof manifest !== "object" || manifest === null || !("server" in manifest)) return [];
|
||||||
const record = manifest as { server?: { createFields?: Array<{ key?: string }> }; runtimeProfiles?: { serverDeployments?: Array<any> } };
|
const record = manifest as { runtimeProfiles?: { serverDeployments?: unknown } };
|
||||||
const declaredFields = new Set((record.server?.createFields ?? []).map((field) => field.key).filter((key): key is string => Boolean(key)));
|
return record.runtimeProfiles?.serverDeployments === undefined ? [] : ["manifest.runtimeProfiles.serverDeployments: legacy server deployment profiles are no longer supported"];
|
||||||
const errors: string[] = [];
|
|
||||||
for (const [index, profile] of (record.runtimeProfiles?.serverDeployments ?? []).entries()) {
|
|
||||||
const location = `manifest.runtimeProfiles.serverDeployments[${index}]`;
|
|
||||||
const mappingKeys = new Set<string>();
|
|
||||||
for (const [mappingIndex, mapping] of (profile.configMappings ?? []).entries()) {
|
|
||||||
const mappingLocation = `${location}.configMappings[${mappingIndex}]`;
|
|
||||||
if (!declaredFields.has(mapping.fieldKey)) errors.push(`${mappingLocation}.fieldKey: must reference a declared server.createFields key`);
|
|
||||||
if (mappingKeys.has(mapping.fieldKey)) errors.push(`${mappingLocation}.fieldKey: duplicate mapping`);
|
|
||||||
mappingKeys.add(mapping.fieldKey);
|
|
||||||
}
|
|
||||||
const requiredChecks = new Set(["executable.present", "port.bound", "config.readable", "process.healthy"]);
|
|
||||||
for (const check of profile.verificationChecks ?? []) {
|
|
||||||
if (check.required) requiredChecks.delete(check.kind);
|
|
||||||
}
|
|
||||||
for (const missing of requiredChecks) errors.push(`${location}.verificationChecks: required check ${missing} is missing`);
|
|
||||||
}
|
|
||||||
return errors;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateUnsupportedLegacyClientManagerDeclarations(manifest: unknown): string[] {
|
function validateUnsupportedLegacyClientManagerDeclarations(manifest: unknown): string[] {
|
||||||
@@ -380,7 +363,7 @@ function validateDLLExtensionProfiles(manifest: unknown): string[] {
|
|||||||
targetKey?: string;
|
targetKey?: string;
|
||||||
modKey?: string;
|
modKey?: string;
|
||||||
dllRef?: string;
|
dllRef?: string;
|
||||||
scumExecutableChecksum?: string;
|
targetExecutableChecksum?: string;
|
||||||
ue4ssAbi?: string;
|
ue4ssAbi?: string;
|
||||||
supportedTargets?: Array<{ os?: string; arch?: string }>;
|
supportedTargets?: Array<{ os?: string; arch?: string }>;
|
||||||
updateOnStart?: boolean;
|
updateOnStart?: boolean;
|
||||||
@@ -415,8 +398,8 @@ function validateDLLExtensionProfiles(manifest: unknown): string[] {
|
|||||||
errors.push(`${location}.releaseState: must be ready or unpublished`);
|
errors.push(`${location}.releaseState: must be ready or unpublished`);
|
||||||
}
|
}
|
||||||
if (profile.releaseState === "ready") {
|
if (profile.releaseState === "ready") {
|
||||||
if (!checksumPattern.test(profile.checksum ?? "") || !checksumPattern.test(profile.scumExecutableChecksum ?? "")) {
|
if (!checksumPattern.test(profile.checksum ?? "") || !checksumPattern.test(profile.targetExecutableChecksum ?? "")) {
|
||||||
errors.push(`${location}: release and SCUM executable SHA-256 checksums are required`);
|
errors.push(`${location}: release and target executable SHA-256 checksums are required`);
|
||||||
}
|
}
|
||||||
if (!Number.isInteger(profile.sizeBytes) || (profile.sizeBytes ?? 0) < 1 || (profile.sizeBytes ?? 0) > 128 * 1024 * 1024) {
|
if (!Number.isInteger(profile.sizeBytes) || (profile.sizeBytes ?? 0) < 1 || (profile.sizeBytes ?? 0) > 128 * 1024 * 1024) {
|
||||||
errors.push(`${location}.sizeBytes: must be a bounded DLL size`);
|
errors.push(`${location}.sizeBytes: must be a bounded DLL size`);
|
||||||
|
|||||||
@@ -387,9 +387,9 @@ export interface RuntimeDependencyProbe {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface RuntimeInstallStep {
|
export interface RuntimeInstallStep {
|
||||||
type: "package" | "verified-download" | "steamcmd-app" | "manual";
|
type: "package" | "verified-download" | "manual";
|
||||||
targetKey: string;
|
targetKey: string;
|
||||||
packageManager?: "winget" | "choco" | "scoop" | "apt" | "yum" | "dnf" | "pacman" | "zypper" | "brew" | "steamcmd" | "manual";
|
packageManager?: "winget" | "choco" | "scoop" | "apt" | "yum" | "dnf" | "pacman" | "zypper" | "brew";
|
||||||
packageName?: string;
|
packageName?: string;
|
||||||
version?: string;
|
version?: string;
|
||||||
downloadRef?: string;
|
downloadRef?: string;
|
||||||
@@ -444,7 +444,7 @@ export interface RuntimeDLLExtensionProfile {
|
|||||||
targetKey: string;
|
targetKey: string;
|
||||||
modKey: string;
|
modKey: string;
|
||||||
dllRef: string;
|
dllRef: string;
|
||||||
scumExecutableChecksum?: `sha256:${string}`;
|
targetExecutableChecksum?: `sha256:${string}`;
|
||||||
ue4ssAbi?: string;
|
ue4ssAbi?: string;
|
||||||
supportedTargets: [{ os: "windows"; arch: "amd64" }];
|
supportedTargets: [{ os: "windows"; arch: "amd64" }];
|
||||||
updateOnStart: true;
|
updateOnStart: true;
|
||||||
|
|||||||
@@ -20,6 +20,9 @@
|
|||||||
{"key": "unsafe-install", "title": "bash -c installer", "steps": [{"type": "manual", "targetKey": "manual"}]},
|
{"key": "unsafe-install", "title": "bash -c installer", "steps": [{"type": "manual", "targetKey": "manual"}]},
|
||||||
{"key": "unsafe-download", "title": "Unsafe dependency download", "steps": [{"type": "verified-download", "targetKey": "tool", "downloadRef": "https://127.0.0.1/tool", "checksum": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}]}
|
{"key": "unsafe-download", "title": "Unsafe dependency download", "steps": [{"type": "verified-download", "targetKey": "tool", "downloadRef": "https://127.0.0.1/tool", "checksum": "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}]}
|
||||||
],
|
],
|
||||||
|
"serverDeployments": [
|
||||||
|
{"key": "legacy", "version": "1.0.0"}
|
||||||
|
],
|
||||||
"clientManagers": [
|
"clientManagers": [
|
||||||
{
|
{
|
||||||
"key": "unsafe-client",
|
"key": "unsafe-client",
|
||||||
|
|||||||
@@ -165,6 +165,7 @@ describe("plugin manifest validation", () => {
|
|||||||
const installScript = fs.readFileSync(path.join(pluginDir, installAction.executableKey), "utf8");
|
const installScript = fs.readFileSync(path.join(pluginDir, installAction.executableKey), "utf8");
|
||||||
const startScript = fs.readFileSync(path.join(pluginDir, startAction.executableKey), "utf8");
|
const startScript = fs.readFileSync(path.join(pluginDir, startAction.executableKey), "utf8");
|
||||||
expect(manifest.runtimeProfiles.serverDeployments).toBeUndefined();
|
expect(manifest.runtimeProfiles.serverDeployments).toBeUndefined();
|
||||||
|
expect(JSON.stringify(manifest.runtimeProfiles.installPlans ?? [])).not.toContain("steamcmd-app");
|
||||||
expect(assetPaths).toEqual(expect.arrayContaining(["actions/install.json", "actions/start.json", "bin/scum-install-update.cmd", "bin/scum-start.cmd", "assets/map/scum-map-overview.jpg"]));
|
expect(assetPaths).toEqual(expect.arrayContaining(["actions/install.json", "actions/start.json", "bin/scum-install-update.cmd", "bin/scum-start.cmd", "assets/map/scum-map-overview.jpg"]));
|
||||||
expect(installAction).toMatchObject({ executableKey: "bin/scum-install-update.cmd", environment: { SERVER_STEAM_APP_ID: "3792580", SERVER_STEAMCMD_UPDATE_ARGS: "+login anonymous +app_update 3792580 +quit" } });
|
expect(installAction).toMatchObject({ executableKey: "bin/scum-install-update.cmd", environment: { SERVER_STEAM_APP_ID: "3792580", SERVER_STEAMCMD_UPDATE_ARGS: "+login anonymous +app_update 3792580 +quit" } });
|
||||||
expect(installAction.timeoutMs).toBe(7200000);
|
expect(installAction.timeoutMs).toBe(7200000);
|
||||||
@@ -709,6 +710,7 @@ describe("plugin manifest validation", () => {
|
|||||||
expect(errors.some((error) => error.includes("raw host path"))).toBe(true);
|
expect(errors.some((error) => error.includes("raw host path"))).toBe(true);
|
||||||
expect(errors.some((error) => error.includes("arbitrary shell"))).toBe(true);
|
expect(errors.some((error) => error.includes("arbitrary shell"))).toBe(true);
|
||||||
expect(errors.some((error) => error.includes("not approved for dependency download"))).toBe(true);
|
expect(errors.some((error) => error.includes("not approved for dependency download"))).toBe(true);
|
||||||
|
expect(errors.some((error) => error.includes("runtimeProfiles.serverDeployments") && error.includes("no longer supported"))).toBe(true);
|
||||||
expect(errors.some((error) => error.includes("runtimeProfiles.clientManagers") && error.includes("no longer supported"))).toBe(true);
|
expect(errors.some((error) => error.includes("runtimeProfiles.clientManagers") && error.includes("no longer supported"))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export PLATFORM_BOOTSTRAP_ADMIN_EMAIL="${PLATFORM_BOOTSTRAP_ADMIN_EMAIL:-operato
|
|||||||
export PLATFORM_BOOTSTRAP_ADMIN_PASSWORD="${PLATFORM_BOOTSTRAP_ADMIN_PASSWORD:-operator-local}"
|
export PLATFORM_BOOTSTRAP_ADMIN_PASSWORD="${PLATFORM_BOOTSTRAP_ADMIN_PASSWORD:-operator-local}"
|
||||||
export PLATFORM_SECRET_ENVELOPE_KEY="${PLATFORM_SECRET_ENVELOPE_KEY:-local-debug-secret-envelope-key-change-me}"
|
export PLATFORM_SECRET_ENVELOPE_KEY="${PLATFORM_SECRET_ENVELOPE_KEY:-local-debug-secret-envelope-key-change-me}"
|
||||||
export PLATFORM_AI_PROVIDER_MODE="${PLATFORM_AI_PROVIDER_MODE:-mock}"
|
export PLATFORM_AI_PROVIDER_MODE="${PLATFORM_AI_PROVIDER_MODE:-mock}"
|
||||||
export PLATFORM_RUN_RELEASE_URL="${PLATFORM_RUN_RELEASE_URL:-https://scum.npc0.com/}"
|
export PLATFORM_RUN_RELEASE_URL="${PLATFORM_RUN_RELEASE_URL:-http://127.0.0.1:$LOCAL_DEBUG_PLATFORM_PORT}"
|
||||||
|
|
||||||
export RUN_SOURCE_DIR="${RUN_SOURCE_DIR:-${RUN_REPO_DIR:-$LOCAL_DEBUG_ROOT_DIR/run}}"
|
export RUN_SOURCE_DIR="${RUN_SOURCE_DIR:-${RUN_REPO_DIR:-$LOCAL_DEBUG_ROOT_DIR/run}}"
|
||||||
export RUN_REPO_DIR="$RUN_SOURCE_DIR"
|
export RUN_REPO_DIR="$RUN_SOURCE_DIR"
|
||||||
|
|||||||
Reference in New Issue
Block a user