Panel — public source drop (v0.9.0)
Self-hostable game-server control panel: controller + agent + 26 game modules. One-line install (prebuilt release, no Go required): curl -fsSL https://git.pdxtechs.com/dbledeez/panel/raw/branch/main/install.sh | sudo bash Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user