package updater import ( "context" "fmt" "strings" "time" "github.com/dbledeez/panel/agent/internal/runtime" "github.com/dbledeez/panel/pkg/steamvdf" ) // FetchAppInfoBranches runs a one-shot SteamCMD sidecar with // // +login anonymous +app_info_update 1 +app_info_print +quit // // captures its stdout, and parses the depots.branches map (branch name → // latest buildid). CHECK-ONLY: no game volumes are mounted, so this can // never touch an install. Only the shared panel-steamcmd-auth volume is // attached (same as update runs) so Steam's client config cache persists // across calls. // // tag disambiguates the sidecar container name per caller (usually the // instance id); appID appears too so parallel checks for different apps // can't collide. func FetchAppInfoBranches(ctx context.Context, rt runtime.Runtime, tag, appID string, log func(string)) (map[string]string, error) { if appID == "" { return nil, fmt.Errorf("app_info: app_id is required") } args := []string{ "+login", "anonymous", "+app_info_update", "1", "+app_info_print", appID, "+quit", } spec := runtime.InstanceSpec{ InstanceID: fmt.Sprintf("%s-appinfo-%s", tag, appID), IsSidecar: true, Image: sidecarImage, Command: args, Volumes: []runtime.VolumeSpec{{ VolumeName: "panel-steamcmd-auth", ContainerPath: "/root/.local/share/Steam", Type: "volume", }}, RestartPolicy: "no", // Same seccomp note as the update sidecar (steamcmd.go): newer // Docker default profiles ENOSYS a syscall the 32-bit Steam // runtime needs. SecurityOpts: []string{"seccomp=unconfined"}, } contID, err := rt.Create(ctx, spec) if err != nil { return nil, fmt.Errorf("create app_info sidecar: %w", err) } defer func() { rmCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() _ = rt.Remove(rmCtx, contID) }() if err := rt.Start(ctx, contID); err != nil { return nil, fmt.Errorf("start app_info sidecar: %w", err) } var out strings.Builder logCtx, cancelLogs := context.WithCancel(ctx) defer cancelLogs() logDone := make(chan struct{}) go func() { defer close(logDone) _ = rt.StreamLogs(logCtx, contID, func(_ runtime.LogStream, line string, _ time.Time) { out.WriteString(line) out.WriteByte('\n') }) }() exitCode, err := rt.Wait(ctx, contID) cancelLogs() <-logDone if err != nil { return nil, fmt.Errorf("wait app_info sidecar: %w", err) } if log != nil { log(fmt.Sprintf("app_info: steamcmd exit %d, %d bytes of output", exitCode, out.Len())) } // SteamCMD sometimes exits non-zero even after printing valid app // info — parse first, only surface the exit code when parsing fails. branches, perr := steamvdf.ParseAppInfoBranches(out.String(), appID) if perr != nil { if exitCode != 0 { return nil, fmt.Errorf("app_info: steamcmd exit %d (%v)", exitCode, perr) } return nil, perr } return branches, nil }