658bda1d24
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
96 lines
2.9 KiB
Go
96 lines
2.9 KiB
Go
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 <appID> +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
|
|
}
|