panel — open-source game server manager (public release)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 19:19:43 -07:00
commit 5232609719
2160 changed files with 300415 additions and 0 deletions
+95
View File
@@ -0,0 +1,95 @@
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
}