Files
BlueArchiveToolkit/internal/api/rpc_release.go
T
nyaKazuha c17904ee1c
bat-rust / Build and test Rust (push) Canceled after 0s
bat-rust / Build and test Go API (push) Canceled after 0s
fix(repo):统一发布健康与只读质量门禁
2026-09-13 22:09:44 +08:00

717 lines
28 KiB
Go

package api
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"bat-api/internal/backendrpc"
)
// Backend is the subset of daemon RPC used by bat-api.
//
// Call order for discovery (per plan review):
// 1. daemon.status
// 2. daemon.doctor
// 3. catalog.status / resource.manifest (and resource.state as needed)
type Backend interface {
DaemonStatus(ctx context.Context) (*backendrpc.DaemonStatusReport, error)
DaemonDoctor(ctx context.Context) (*backendrpc.DoctorReport, error)
ResourceState(ctx context.Context) (*backendrpc.ResourceState, error)
CatalogStatus(ctx context.Context) (json.RawMessage, error)
ResourceManifest(ctx context.Context, offset int, limit int) (*backendrpc.ResourceManifestPage, error)
}
// ControlBackend is the explicitly allowlisted mutation subset exposed through
// the authenticated bat-api admin control surface.
//
// It intentionally does not include daemon.stop, cleanup, or generic RPC calls.
// Restart is forwarded only to Rust's lifecycle RPC; Go never execs bat itself.
type ControlBackend interface {
DaemonRestart(ctx context.Context) (*backendrpc.Ack, error)
DaemonReload(ctx context.Context) (*backendrpc.Ack, error)
DaemonRefresh(ctx context.Context, force bool) (*backendrpc.Ack, error)
ResourceSync(ctx context.Context, force bool) (*backendrpc.TaskAccepted, error)
ResourceVerify(ctx context.Context) (*backendrpc.TaskAccepted, error)
ResourceRepair(ctx context.Context) (*backendrpc.TaskAccepted, error)
CatalogRefresh(ctx context.Context, force bool) (*backendrpc.TaskAccepted, error)
}
// DaemonLogsBackend exposes the Rust daemon log tail for an authenticated
// dashboard. It remains read-only and never opens log files from Go.
type DaemonLogsBackend interface {
DaemonLogs(ctx context.Context, tail int) (*backendrpc.LogsReport, error)
}
// TaskBackend exposes Rust-owned async task state to the dashboard. Go only
// forwards read/cancel requests and does not create generic tasks.
type TaskBackend interface {
TaskList(ctx context.Context) (*backendrpc.TaskList, error)
TaskStatus(ctx context.Context, taskID string) (*backendrpc.TaskRecord, error)
TaskLogs(ctx context.Context, taskID string) (*backendrpc.TaskLogs, error)
TaskCancel(ctx context.Context, taskID string) (*backendrpc.TaskCancelResult, error)
}
// ParseBackend exposes existing Rust TextUnit index queries to the dashboard.
// It is read-only and does not expand parser coverage.
type ParseBackend interface {
ParseStatus(ctx context.Context) (json.RawMessage, error)
ParseTextUnits(ctx context.Context, query backendrpc.TextUnitQueryParams) (json.RawMessage, error)
ParseErrors(ctx context.Context, query backendrpc.TextUnitQueryParams) (json.RawMessage, error)
}
// ScheduleBackend exposes the Rust-owned schedule store to an authenticated
// dashboard. The JSON result remains Rust's report shape so the API does not
// duplicate schedule state or invent a second schema.
type ScheduleBackend interface {
ScheduleList(ctx context.Context, params backendrpc.ScheduleListParams) (json.RawMessage, error)
ScheduleAdd(ctx context.Context, params backendrpc.ScheduleMutationParams) (json.RawMessage, error)
ScheduleUpdate(ctx context.Context, params backendrpc.ScheduleMutationParams) (json.RawMessage, error)
ScheduleRemove(ctx context.Context, params backendrpc.ScheduleMutationParams) (json.RawMessage, error)
ScheduleRun(ctx context.Context, params backendrpc.ScheduleRunParams) (json.RawMessage, error)
}
// TranslationBackend exposes the narrow provider-worker state mutation used
// by the dashboard. It does not create arbitrary translation jobs.
type TranslationBackend interface {
TranslationTaskUpdate(ctx context.Context, params backendrpc.TranslationTaskUpdateParams) (json.RawMessage, error)
TranslationTasks(ctx context.Context, params backendrpc.TranslationTaskListParams) (json.RawMessage, error)
TranslationHandoff(ctx context.Context) (json.RawMessage, error)
TranslationWorkerRun(ctx context.Context, params backendrpc.TranslationWorkerRunParams) (*backendrpc.TranslationWorkerRunResult, error)
TranslationProofread(ctx context.Context) (json.RawMessage, error)
}
// TranslationMemoryBackend exposes the Rust-owned Translation Memory query and
// explicit confirmation operations. Go forwards typed requests and responses
// but never opens or mutates the TM database itself.
type TranslationMemoryBackend interface {
TranslationMemorySummary(ctx context.Context, params backendrpc.TranslationMemorySummaryParams) (*backendrpc.TranslationMemorySummaryReport, error)
TranslationMemoryQuery(ctx context.Context, params backendrpc.TranslationMemoryQueryParams) (*backendrpc.TranslationMemoryQueryReport, error)
TranslationMemoryConfirm(ctx context.Context, params backendrpc.TranslationMemoryConfirmParams) (*backendrpc.TranslationMemoryConfirmReport, error)
}
// GlossaryBackend exposes Rust-owned Glossary management and diagnostics.
// Go forwards these typed calls and never opens the Glossary database.
type GlossaryBackend interface {
GlossarySummary(ctx context.Context, params backendrpc.GlossarySummaryParams) (*backendrpc.GlossarySummaryReport, error)
GlossaryQuery(ctx context.Context, params backendrpc.GlossaryQueryParams) (*backendrpc.GlossaryQueryReport, error)
GlossaryDiagnose(ctx context.Context, params backendrpc.GlossaryDiagnoseParams) (*backendrpc.GlossaryDiagnoseReport, error)
GlossaryAdd(ctx context.Context, params backendrpc.GlossaryTermMutationParams) (*backendrpc.GlossaryMutationReport, error)
GlossaryUpdate(ctx context.Context, params backendrpc.GlossaryTermMutationParams) (*backendrpc.GlossaryMutationReport, error)
GlossaryApprove(ctx context.Context, params backendrpc.GlossaryReviewParams) (*backendrpc.GlossaryMutationReport, error)
GlossaryDeprecate(ctx context.Context, params backendrpc.GlossaryReviewParams) (*backendrpc.GlossaryMutationReport, error)
GlossaryDelete(ctx context.Context, params backendrpc.GlossaryDeleteParams) (*backendrpc.GlossaryMutationReport, error)
}
// LocalizedBackend exposes localized release status and the explicit
// publish/rollback controls used by the authenticated dashboard.
type LocalizedBackend interface {
LocalizedStatus(ctx context.Context) (json.RawMessage, error)
LocalizedPublish(ctx context.Context, params backendrpc.LocalizedPublishParams) (json.RawMessage, error)
LocalizedRollback(ctx context.Context, params backendrpc.LocalizedRollbackParams) (json.RawMessage, error)
}
// ReleaseStatusBackend exposes the Rust-owned release health fact used during
// discovery. It is kept separate so lightweight test/diagnostic backends do
// not have to implement the administrative release surface.
type ReleaseStatusBackend interface {
ReleaseStatus(ctx context.Context) (*backendrpc.ReleaseStatusReport, error)
}
// ReleaseBackend exposes Rust-owned dual-release queries, distribution
// selection and the explicit cleanup operation.
type ReleaseBackend interface {
ReleaseStatusBackend
ReleaseList(ctx context.Context, params backendrpc.ReleaseListParams) (*backendrpc.ReleaseListReport, error)
ReleaseDistribution(ctx context.Context, params backendrpc.ReleaseDistributionParams) (*backendrpc.ReleaseDistributionPage, error)
ReleaseCleanup(ctx context.Context, params backendrpc.ReleaseCleanupParams) (*backendrpc.ReleaseCleanupReport, error)
}
// RPCClient adapts *backendrpc.Client to Backend.
type RPCClient struct {
Client *backendrpc.Client
}
func (r RPCClient) DaemonStatus(ctx context.Context) (*backendrpc.DaemonStatusReport, error) {
return r.Client.DaemonStatus(ctx)
}
func (r RPCClient) DaemonDoctor(ctx context.Context) (*backendrpc.DoctorReport, error) {
return r.Client.DaemonDoctor(ctx)
}
func (r RPCClient) ResourceState(ctx context.Context) (*backendrpc.ResourceState, error) {
return r.Client.ResourceState(ctx)
}
func (r RPCClient) CatalogStatus(ctx context.Context) (json.RawMessage, error) {
return r.Client.CatalogStatus(ctx)
}
func (r RPCClient) ResourceManifest(ctx context.Context, offset int, limit int) (*backendrpc.ResourceManifestPage, error) {
return r.Client.ResourceManifest(ctx, offset, limit)
}
func (r RPCClient) DaemonRestart(ctx context.Context) (*backendrpc.Ack, error) {
return r.Client.DaemonRestart(ctx)
}
func (r RPCClient) DaemonReload(ctx context.Context) (*backendrpc.Ack, error) {
return r.Client.DaemonReload(ctx)
}
func (r RPCClient) DaemonRefresh(ctx context.Context, force bool) (*backendrpc.Ack, error) {
return r.Client.DaemonRefresh(ctx, force)
}
func (r RPCClient) DaemonLogs(ctx context.Context, tail int) (*backendrpc.LogsReport, error) {
return r.Client.DaemonLogs(ctx, tail)
}
func (r RPCClient) ResourceSync(ctx context.Context, force bool) (*backendrpc.TaskAccepted, error) {
return r.Client.ResourceSync(ctx, force)
}
func (r RPCClient) ResourceVerify(ctx context.Context) (*backendrpc.TaskAccepted, error) {
return r.Client.ResourceVerify(ctx)
}
func (r RPCClient) ResourceRepair(ctx context.Context) (*backendrpc.TaskAccepted, error) {
return r.Client.ResourceRepair(ctx)
}
func (r RPCClient) CatalogRefresh(ctx context.Context, force bool) (*backendrpc.TaskAccepted, error) {
return r.Client.CatalogRefresh(ctx, force)
}
func (r RPCClient) TaskList(ctx context.Context) (*backendrpc.TaskList, error) {
return r.Client.TaskList(ctx)
}
func (r RPCClient) TaskStatus(ctx context.Context, taskID string) (*backendrpc.TaskRecord, error) {
return r.Client.TaskStatus(ctx, taskID)
}
func (r RPCClient) TaskLogs(ctx context.Context, taskID string) (*backendrpc.TaskLogs, error) {
return r.Client.TaskLogs(ctx, taskID)
}
func (r RPCClient) TaskCancel(ctx context.Context, taskID string) (*backendrpc.TaskCancelResult, error) {
return r.Client.TaskCancel(ctx, taskID)
}
func (r RPCClient) ScheduleList(ctx context.Context, params backendrpc.ScheduleListParams) (json.RawMessage, error) {
return r.Client.ScheduleListFiltered(ctx, params)
}
func (r RPCClient) ScheduleAdd(ctx context.Context, params backendrpc.ScheduleMutationParams) (json.RawMessage, error) {
return r.Client.ScheduleAdd(ctx, params)
}
func (r RPCClient) ScheduleUpdate(ctx context.Context, params backendrpc.ScheduleMutationParams) (json.RawMessage, error) {
return r.Client.ScheduleUpdate(ctx, params)
}
func (r RPCClient) ScheduleRemove(ctx context.Context, params backendrpc.ScheduleMutationParams) (json.RawMessage, error) {
return r.Client.ScheduleRemove(ctx, params)
}
func (r RPCClient) ScheduleRun(ctx context.Context, params backendrpc.ScheduleRunParams) (json.RawMessage, error) {
return r.Client.ScheduleRun(ctx, params)
}
func (r RPCClient) TranslationTaskUpdate(ctx context.Context, params backendrpc.TranslationTaskUpdateParams) (json.RawMessage, error) {
return r.Client.TranslationTaskUpdate(ctx, params)
}
func (r RPCClient) TranslationTasks(ctx context.Context, params backendrpc.TranslationTaskListParams) (json.RawMessage, error) {
return r.Client.TranslationTasks(ctx, params)
}
func (r RPCClient) TranslationHandoff(ctx context.Context) (json.RawMessage, error) {
return r.Client.TranslationHandoff(ctx)
}
func (r RPCClient) TranslationWorkerRun(ctx context.Context, params backendrpc.TranslationWorkerRunParams) (*backendrpc.TranslationWorkerRunResult, error) {
return r.Client.TranslationWorkerRun(ctx, params)
}
func (r RPCClient) TranslationProofread(ctx context.Context) (json.RawMessage, error) {
return r.Client.TranslationProofread(ctx)
}
func (r RPCClient) TranslationMemorySummary(ctx context.Context, params backendrpc.TranslationMemorySummaryParams) (*backendrpc.TranslationMemorySummaryReport, error) {
return r.Client.TranslationMemorySummary(ctx, params)
}
func (r RPCClient) TranslationMemoryQuery(ctx context.Context, params backendrpc.TranslationMemoryQueryParams) (*backendrpc.TranslationMemoryQueryReport, error) {
return r.Client.TranslationMemoryQuery(ctx, params)
}
func (r RPCClient) TranslationMemoryConfirm(ctx context.Context, params backendrpc.TranslationMemoryConfirmParams) (*backendrpc.TranslationMemoryConfirmReport, error) {
return r.Client.TranslationMemoryConfirm(ctx, params)
}
func (r RPCClient) GlossarySummary(ctx context.Context, params backendrpc.GlossarySummaryParams) (*backendrpc.GlossarySummaryReport, error) {
return r.Client.GlossarySummary(ctx, params)
}
func (r RPCClient) GlossaryQuery(ctx context.Context, params backendrpc.GlossaryQueryParams) (*backendrpc.GlossaryQueryReport, error) {
return r.Client.GlossaryQuery(ctx, params)
}
func (r RPCClient) GlossaryDiagnose(ctx context.Context, params backendrpc.GlossaryDiagnoseParams) (*backendrpc.GlossaryDiagnoseReport, error) {
return r.Client.GlossaryDiagnose(ctx, params)
}
func (r RPCClient) GlossaryAdd(ctx context.Context, params backendrpc.GlossaryTermMutationParams) (*backendrpc.GlossaryMutationReport, error) {
return r.Client.GlossaryAdd(ctx, params)
}
func (r RPCClient) GlossaryUpdate(ctx context.Context, params backendrpc.GlossaryTermMutationParams) (*backendrpc.GlossaryMutationReport, error) {
return r.Client.GlossaryUpdate(ctx, params)
}
func (r RPCClient) GlossaryApprove(ctx context.Context, params backendrpc.GlossaryReviewParams) (*backendrpc.GlossaryMutationReport, error) {
return r.Client.GlossaryApprove(ctx, params)
}
func (r RPCClient) GlossaryDeprecate(ctx context.Context, params backendrpc.GlossaryReviewParams) (*backendrpc.GlossaryMutationReport, error) {
return r.Client.GlossaryDeprecate(ctx, params)
}
func (r RPCClient) GlossaryDelete(ctx context.Context, params backendrpc.GlossaryDeleteParams) (*backendrpc.GlossaryMutationReport, error) {
return r.Client.GlossaryDelete(ctx, params)
}
func (r RPCClient) LocalizedStatus(ctx context.Context) (json.RawMessage, error) {
return r.Client.LocalizedStatus(ctx)
}
func (r RPCClient) LocalizedPublish(ctx context.Context, params backendrpc.LocalizedPublishParams) (json.RawMessage, error) {
return r.Client.LocalizedPublish(ctx, params)
}
func (r RPCClient) LocalizedRollback(ctx context.Context, params backendrpc.LocalizedRollbackParams) (json.RawMessage, error) {
return r.Client.LocalizedRollback(ctx, params)
}
func (r RPCClient) ReleaseStatus(ctx context.Context) (*backendrpc.ReleaseStatusReport, error) {
return r.Client.ReleaseStatus(ctx)
}
func (r RPCClient) ReleaseList(ctx context.Context, params backendrpc.ReleaseListParams) (*backendrpc.ReleaseListReport, error) {
return r.Client.ReleaseList(ctx, params)
}
func (r RPCClient) ReleaseDistribution(ctx context.Context, params backendrpc.ReleaseDistributionParams) (*backendrpc.ReleaseDistributionPage, error) {
return r.Client.ReleaseDistribution(ctx, params)
}
func (r RPCClient) ReleaseCleanup(ctx context.Context, params backendrpc.ReleaseCleanupParams) (*backendrpc.ReleaseCleanupReport, error) {
return r.Client.ReleaseCleanup(ctx, params)
}
func (r RPCClient) ParseStatus(ctx context.Context) (json.RawMessage, error) {
return r.Client.ParseStatus(ctx)
}
func (r RPCClient) ParseTextUnits(ctx context.Context, query backendrpc.TextUnitQueryParams) (json.RawMessage, error) {
return r.Client.ParseTextUnits(ctx, query)
}
func (r RPCClient) ParseErrors(ctx context.Context, query backendrpc.TextUnitQueryParams) (json.RawMessage, error) {
return r.Client.ParseErrors(ctx, query)
}
func (r RPCClient) UnityFSPatchTextAsset(ctx context.Context, params backendrpc.UnityFSTextAssetPatchParams) (json.RawMessage, error) {
return r.Client.UnityFSPatchTextAsset(ctx, params)
}
func (r RPCClient) UnityFSPatchStringField(ctx context.Context, params backendrpc.UnityFSStringFieldPatchParams) (json.RawMessage, error) {
return r.Client.UnityFSPatchStringField(ctx, params)
}
func (r RPCClient) UnityFSPatchField(ctx context.Context, params backendrpc.UnityFSFieldPatchParams) (json.RawMessage, error) {
return r.Client.UnityFSPatchField(ctx, params)
}
// DiscoverResult is the outcome of talking to the bat daemon.
type DiscoverResult struct {
RPCAvailable bool
DoctorHealthy *bool
Status *backendrpc.DaemonStatusReport
Doctor *backendrpc.DoctorReport
ReleaseStatus *backendrpc.ReleaseStatusReport
Distribution DistributionHealth
Snapshot *SnapshotSummary
ResourceRoot string
Index *ReleaseIndex
Warnings []string
}
// DiscoverAndIndex contacts the daemon (status first, then doctor, then
// release.status) and builds a release index from paginated resource.manifest
// plus on-disk checks. Rust's release.status is the only release-level
// integrity authorization used for the production RPC path.
//
// If resourceRootOverride is non-empty, it wins over RPC-reported roots after
// RPC health probes (still preferred for production to call status/doctor).
func DiscoverAndIndex(ctx context.Context, backend Backend, resourceRootOverride string) (*DiscoverResult, error) {
out := &DiscoverResult{}
if backend == nil {
if resourceRootOverride == "" {
out.Index = &ReleaseIndex{Source: "empty", byRel: map[string]int{}}
return out, nil
}
idx, err := LoadIndexFromResourceRoot(resourceRootOverride)
if err != nil {
return nil, err
}
out.ResourceRoot = idx.ResourceRoot
out.Distribution = idx.Distribution
out.Index = idx
return out, nil
}
// 1) daemon.status first
status, err := backend.DaemonStatus(ctx)
if err != nil {
out.Warnings = append(out.Warnings, fmt.Sprintf("daemon.status: %v", err))
if resourceRootOverride != "" {
idx, loadErr := LoadIndexFromResourceRoot(resourceRootOverride)
if loadErr != nil {
return out, fmt.Errorf("daemon.status failed (%v) and resource-root load failed: %w", err, loadErr)
}
out.ResourceRoot = idx.ResourceRoot
out.Distribution = idx.Distribution
out.Index = idx
return out, nil
}
out.Index = &ReleaseIndex{Source: "empty", byRel: map[string]int{}}
return out, nil
}
out.RPCAvailable = true
out.Status = status
// 2) daemon.doctor second
doctor, err := backend.DaemonDoctor(ctx)
if err != nil {
out.Warnings = append(out.Warnings, fmt.Sprintf("daemon.doctor: %v", err))
} else {
out.Doctor = doctor
h := doctor.Healthy
out.DoctorHealthy = &h
}
// An explicit root is a fixture/emergency read-only override. Keep it
// outside the production RPC release-health contract, while still probing
// daemon status and doctor first.
if resourceRootOverride != "" {
idx, loadErr := LoadIndexFromResourceRoot(resourceRootOverride)
if loadErr != nil {
return out, fmt.Errorf("resource-root override load failed: %w", loadErr)
}
idx.Source = "resource_root"
idx.RPCAvailable = true
idx.DoctorHealthy = out.DoctorHealthy
out.ResourceRoot = idx.ResourceRoot
out.Snapshot = idx.Snapshot
out.Distribution = idx.Distribution
out.Index = idx
return out, nil
}
// 3) release.status is the Rust-owned whole-release distribution gate.
releaseStatusBackend, ok := backend.(ReleaseStatusBackend)
if !ok {
out.Warnings = append(out.Warnings, "release.status: backend does not expose Rust release health")
return emptyRPCResult(out, nil, "Rust release health is unavailable"),
fmt.Errorf("rust release health is unavailable")
}
releaseStatus, err := releaseStatusBackend.ReleaseStatus(ctx)
if err != nil {
out.Warnings = append(out.Warnings, fmt.Sprintf("release.status: %v", err))
return emptyRPCResult(out, nil, "Rust release health query failed"),
fmt.Errorf("release.status failed: %w", err)
}
if releaseStatus == nil {
out.Warnings = append(out.Warnings, "release.status: empty response")
return emptyRPCResult(out, nil, "Rust release health query returned no response"),
fmt.Errorf("release.status returned an empty response")
}
out.ReleaseStatus = releaseStatus
out.Distribution = rustDistributionHealth(releaseStatus)
// Catalog / resource discovery
var snapshot *SnapshotSummary
var resourceRoot string
catalogAvailabilityKnown := false
catalogAvailable := false
if raw, err := backend.CatalogStatus(ctx); err != nil {
out.Warnings = append(out.Warnings, fmt.Sprintf("catalog.status: %v", err))
} else {
catalogAvailable, catalogAvailabilityKnown = parseCatalogAvailability(raw)
snap, root, ok := parseCatalogStatus(raw)
if ok {
snapshot = snap
resourceRoot = root
}
}
if snapshot != nil &&
snapshot.VersionID != "" &&
out.ReleaseStatus.OfficialCurrentReleaseID != "" &&
snapshot.VersionID != out.ReleaseStatus.OfficialCurrentReleaseID {
return emptyRPCResult(
out,
snapshot,
fmt.Sprintf(
"release.status current ID %q does not match catalog current ID %q",
out.ReleaseStatus.OfficialCurrentReleaseID,
snapshot.VersionID,
),
), nil
}
if catalogAvailabilityKnown && !catalogAvailable {
return emptyRPCResult(out, snapshot, "catalog.status available=false; no published release"), nil
}
if resourceRoot == "" {
if state, err := backend.ResourceState(ctx); err != nil {
out.Warnings = append(out.Warnings, fmt.Sprintf("resource.state: %v", err))
} else if state.ResourceOutputRoot != nil && *state.ResourceOutputRoot != "" {
// Prefer published current under output root when catalog root missing.
candidate := filepath.Join(*state.ResourceOutputRoot, "current")
resourceRoot = candidate
}
}
if resourceRootOverride != "" {
resourceRoot = resourceRootOverride
}
if resourceRoot == "" {
out.Snapshot = snapshotWithDistributionHealth(snapshot, out.Distribution)
out.Index = &ReleaseIndex{
Source: "rpc",
RPCAvailable: true,
DoctorHealthy: out.DoctorHealthy,
Distribution: out.Distribution,
Snapshot: snapshotWithDistributionHealth(snapshot, out.Distribution),
byRel: map[string]int{},
}
out.Warnings = append(out.Warnings, "no resource root from RPC; set --resource-root or publish a version")
return out, nil
}
if resourceRootOverride == "" {
info, statErr := os.Stat(resourceRoot)
if statErr != nil || !info.IsDir() {
warning := fmt.Sprintf("published resource root is unavailable: %s", resourceRoot)
if statErr != nil {
warning = fmt.Sprintf("%s (%v)", warning, statErr)
} else {
warning = fmt.Sprintf("%s (not a directory)", warning)
}
return emptyRPCResult(out, snapshot, warning), nil
}
}
entries, manifestVersion, rootFromManifest, err := fetchAllManifestEntries(ctx, backend)
if err != nil {
out.Warnings = append(out.Warnings, fmt.Sprintf("resource.manifest: %v", err))
// Without the RPC manifest there is no evidence that the local
// snapshot matches the Rust health fact. Do not pair a fresh health
// result with a potentially stale on-disk manifest.
return emptyRPCResult(
out,
snapshot,
fmt.Sprintf("published release cannot be indexed: %v", err),
), nil
}
if rootFromManifest != "" {
resourceRoot = rootFromManifest
}
if resourceRootOverride != "" {
resourceRoot = resourceRootOverride
}
idx, err := BuildIndexFromManifestEntries(
resourceRoot,
"rpc",
true,
out.DoctorHealthy,
snapshot,
manifestVersion,
entries,
out.Distribution,
)
if err != nil {
if resourceRootOverride == "" {
return emptyRPCResult(
out,
snapshot,
fmt.Sprintf("published release failed local validation: %v", err),
), nil
}
return out, err
}
out.ResourceRoot = idx.ResourceRoot
out.Snapshot = idx.Snapshot
out.Index = idx
return out, nil
}
func emptyRPCResult(out *DiscoverResult, snapshot *SnapshotSummary, warning string) *DiscoverResult {
distribution := out.Distribution
if distribution.Source == "" {
distribution = unavailableRustDistributionHealth()
out.Distribution = distribution
}
snapshot = snapshotWithDistributionHealth(snapshot, distribution)
out.ResourceRoot = ""
out.Snapshot = snapshot
out.Index = &ReleaseIndex{
Source: "rpc",
RPCAvailable: true,
DoctorHealthy: out.DoctorHealthy,
Distribution: distribution,
Snapshot: snapshot,
byRel: map[string]int{},
}
if warning != "" {
out.Warnings = append(out.Warnings, warning)
}
return out
}
func rustDistributionHealth(report *backendrpc.ReleaseStatusReport) DistributionHealth {
health := DistributionHealth{
Source: "rust_release_status",
Status: "blocked",
StatusCode: "distribution.blocked",
IntegrityStatus: "unknown",
}
if report == nil {
return health
}
health.Ready = report.OfficialDistributionReady
health.Status = report.Status
health.StatusCode = report.StatusCode
if health.Status == "" {
if health.Ready {
health.Status = "ready"
} else {
health.Status = "blocked"
}
}
if health.StatusCode == "" {
if health.Ready {
health.StatusCode = "distribution.ready"
} else {
health.StatusCode = "distribution.blocked"
}
}
for _, release := range report.Releases {
if release.Channel == "official" && release.Current {
health.IntegrityStatus = release.DistributionIntegrityStatus
health.Diagnostics = append([]string(nil), release.Diagnostics...)
break
}
}
if health.IntegrityStatus == "valid" && !health.Ready {
health.IntegrityStatus = "invalid"
}
return health
}
func unavailableRustDistributionHealth() DistributionHealth {
return DistributionHealth{
Source: "rust_release_status",
Status: "unavailable",
StatusCode: "distribution.health_unavailable",
IntegrityStatus: "unknown",
}
}
func snapshotWithDistributionHealth(snapshot *SnapshotSummary, health DistributionHealth) *SnapshotSummary {
if snapshot == nil {
return nil
}
updated := *snapshot
if health.Status != "" {
updated.DistributionStatus = health.Status
}
if health.StatusCode != "" {
updated.DistributionStatusCode = health.StatusCode
}
return &updated
}
func parseCatalogStatus(raw json.RawMessage) (*SnapshotSummary, string, bool) {
if len(raw) == 0 || string(raw) == "null" {
return nil, "", false
}
var payload struct {
Available bool `json:"available"`
Status string `json:"status"`
StatusCode string `json:"status_code"`
DistributionStatus string `json:"distribution_status"`
DistributionStatusCode string `json:"distribution_status_code"`
AppVersion string `json:"app_version"`
BundleVersion string `json:"bundle_version"`
ConnectionGroupName string `json:"connection_group_name"`
AddressablesRoot string `json:"addressables_root"`
LauncherMetadata *LauncherMetadataSummary `json:"launcher_metadata"`
GameMainConfigBootstrap *GameMainConfigSummary `json:"game_main_config_bootstrap"`
Version *struct {
ID string `json:"id"`
CompletedUnixSeconds *uint64 `json:"completed_unix_seconds"`
ResourceRoot string `json:"resource_root"`
} `json:"version"`
}
if err := json.Unmarshal(raw, &payload); err != nil || !payload.Available {
return nil, "", false
}
snap := &SnapshotSummary{
Status: payload.Status,
StatusCode: payload.StatusCode,
DistributionStatus: payload.DistributionStatus,
DistributionStatusCode: payload.DistributionStatusCode,
AppVersion: payload.AppVersion,
BundleVersion: payload.BundleVersion,
ConnectionGroupName: payload.ConnectionGroupName,
AddressablesRoot: payload.AddressablesRoot,
LauncherMetadata: payload.LauncherMetadata,
GameMainConfig: payload.GameMainConfigBootstrap,
}
root := ""
if payload.Version != nil {
snap.VersionID = payload.Version.ID
snap.CompletedUnixSeconds = payload.Version.CompletedUnixSeconds
root = payload.Version.ResourceRoot
}
return snap, root, true
}
func parseCatalogAvailability(raw json.RawMessage) (bool, bool) {
if len(raw) == 0 || string(raw) == "null" {
return false, false
}
var payload struct {
Available *bool `json:"available"`
}
if err := json.Unmarshal(raw, &payload); err != nil || payload.Available == nil {
return false, false
}
return *payload.Available, true
}
func fetchAllManifestEntries(ctx context.Context, backend Backend) ([]manifestEntry, int, string, error) {
const pageSize = 500
offset := 0
var all []manifestEntry
var version int
var root string
for {
page, err := backend.ResourceManifest(ctx, offset, pageSize)
if err != nil {
return nil, 0, "", err
}
if !page.Available {
return nil, 0, "", fmt.Errorf("resource.manifest available=false")
}
if root == "" {
root = page.ResourceRoot
}
if version == 0 {
version = page.ManifestVersion
}
for _, e := range page.Entries {
var bytes uint64
if e.Bytes != nil {
bytes = *e.Bytes
}
all = append(all, manifestEntry{
URL: e.URL,
Destination: e.Destination,
Bytes: bytes,
BLAKE3: e.BLAKE3,
})
}
offset += len(page.Entries)
if len(page.Entries) == 0 || offset >= page.TotalEntries {
break
}
}
return all, version, root, nil
}