2026-05-22 20:26:11 -04:00
|
|
|
package handlers
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"log/slog"
|
|
|
|
|
"net/http"
|
2026-08-04 17:33:15 +02:00
|
|
|
"sync"
|
2026-05-22 20:26:11 -04:00
|
|
|
|
|
|
|
|
"github.com/Silo-Server/silo-server/internal/buildinfo"
|
|
|
|
|
"github.com/Silo-Server/silo-server/internal/nodepool"
|
|
|
|
|
"github.com/Silo-Server/silo-server/internal/playback"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// SystemHandler serves read-only system inspection endpoints.
|
|
|
|
|
type SystemHandler struct {
|
|
|
|
|
transcodePool *nodepool.TranscodePool
|
|
|
|
|
jwtSecret string
|
2026-06-08 02:27:56 +01:00
|
|
|
ffmpegPath string
|
2026-05-22 20:26:11 -04:00
|
|
|
buildInfo buildinfo.Info
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewSystemHandler creates a SystemHandler.
|
2026-06-08 02:27:56 +01:00
|
|
|
func NewSystemHandler(transcodePool *nodepool.TranscodePool, jwtSecret string, ffmpegPath string) *SystemHandler {
|
2026-05-22 20:26:11 -04:00
|
|
|
return &SystemHandler{
|
|
|
|
|
transcodePool: transcodePool,
|
|
|
|
|
jwtSecret: jwtSecret,
|
2026-06-08 02:27:56 +01:00
|
|
|
ffmpegPath: ffmpegPath,
|
2026-05-22 20:26:11 -04:00
|
|
|
buildInfo: buildinfo.Current(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-04 17:33:15 +02:00
|
|
|
// NodeHWAccel reports one transcode node's GPU inventory.
|
|
|
|
|
type NodeHWAccel struct {
|
|
|
|
|
NodeURL string `json:"node_url"`
|
|
|
|
|
NodeName string `json:"node_name,omitempty"`
|
|
|
|
|
Resolved string `json:"resolved,omitempty"`
|
|
|
|
|
RenderDevices []string `json:"render_devices,omitempty"`
|
|
|
|
|
RenderDeviceDetails []playback.RenderDeviceInfo `json:"render_device_details,omitempty"`
|
|
|
|
|
Error string `json:"error,omitempty"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// HWAccelInventory is the admin hw-accel response: the primary probe (flat,
|
|
|
|
|
// backward compatible with the previous single-probe response) plus every
|
|
|
|
|
// healthy node's inventory. playback.hw_device is one cluster-wide value, so
|
|
|
|
|
// the UI needs all node inventories to warn when they diverge (devices must
|
|
|
|
|
// exist at identical paths on every node for the setting to be safe).
|
|
|
|
|
type HWAccelInventory struct {
|
|
|
|
|
playback.HWAccelInfo
|
|
|
|
|
Nodes []NodeHWAccel `json:"nodes,omitempty"`
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-22 20:26:11 -04:00
|
|
|
// HandleHWAccel handles GET /admin/system/hw-accel.
|
2026-08-04 17:33:15 +02:00
|
|
|
// With transcode nodes registered it probes every healthy node (the primary
|
|
|
|
|
// flat fields come from the first that responds, preserving the historical
|
|
|
|
|
// shape); with none it probes the local host.
|
2026-05-22 20:26:11 -04:00
|
|
|
func (h *SystemHandler) HandleHWAccel(w http.ResponseWriter, r *http.Request) {
|
2026-08-04 17:33:15 +02:00
|
|
|
var healthy []*nodepool.Node
|
2026-05-22 20:26:11 -04:00
|
|
|
if h.transcodePool != nil {
|
2026-08-04 17:33:15 +02:00
|
|
|
for _, node := range h.transcodePool.Nodes() {
|
|
|
|
|
if node != nil && node.Enabled && node.Healthy {
|
|
|
|
|
healthy = append(healthy, node)
|
2026-05-22 20:26:11 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-04 17:33:15 +02:00
|
|
|
if len(healthy) == 0 {
|
|
|
|
|
writeJSON(w, http.StatusOK, HWAccelInventory{HWAccelInfo: playback.DetectHWAccelWithFFmpeg(h.ffmpegPath)})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
inventory := HWAccelInventory{Nodes: make([]NodeHWAccel, len(healthy))}
|
|
|
|
|
infos := make([]playback.HWAccelInfo, len(healthy))
|
|
|
|
|
errs := make([]error, len(healthy))
|
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
|
for i, node := range healthy {
|
|
|
|
|
wg.Add(1)
|
|
|
|
|
go func() {
|
|
|
|
|
defer wg.Done()
|
|
|
|
|
infos[i], errs[i] = h.fetchRemoteHWAccel(r.Context(), node)
|
|
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
wg.Wait()
|
2026-05-22 20:26:11 -04:00
|
|
|
|
2026-08-04 17:33:15 +02:00
|
|
|
for i, node := range healthy {
|
|
|
|
|
entry := NodeHWAccel{NodeURL: node.URL, NodeName: node.Name}
|
|
|
|
|
if errs[i] != nil {
|
|
|
|
|
slog.WarnContext(r.Context(), "hw-accel: node probe failed", "component", "api",
|
|
|
|
|
"node", node.URL, "error", errs[i])
|
|
|
|
|
entry.Error = errs[i].Error()
|
|
|
|
|
} else {
|
|
|
|
|
entry.Resolved = infos[i].Resolved
|
|
|
|
|
entry.RenderDevices = infos[i].RenderDevices
|
|
|
|
|
entry.RenderDeviceDetails = infos[i].RenderDeviceDetails
|
|
|
|
|
}
|
|
|
|
|
inventory.Nodes[i] = entry
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Primary flat fields: the first node that answered; if none did, fall
|
|
|
|
|
// back to a local probe (matching the historical fallback behavior).
|
|
|
|
|
primaried := false
|
|
|
|
|
for i := range healthy {
|
|
|
|
|
if errs[i] == nil {
|
|
|
|
|
inventory.HWAccelInfo = infos[i]
|
|
|
|
|
primaried = true
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if !primaried {
|
|
|
|
|
inventory.HWAccelInfo = playback.DetectHWAccelWithFFmpeg(h.ffmpegPath)
|
|
|
|
|
}
|
|
|
|
|
writeJSON(w, http.StatusOK, inventory)
|
2026-05-22 20:26:11 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// HandleBuildInfo handles GET /admin/system/build.
|
|
|
|
|
func (h *SystemHandler) HandleBuildInfo(w http.ResponseWriter, _ *http.Request) {
|
|
|
|
|
writeJSON(w, http.StatusOK, h.buildInfo)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *SystemHandler) fetchRemoteHWAccel(ctx context.Context, node *nodepool.Node) (playback.HWAccelInfo, error) {
|
2026-07-14 11:51:27 -04:00
|
|
|
return fetchRemoteTranscodeCapabilities(ctx, node.URL, h.jwtSecret)
|
2026-05-22 20:26:11 -04:00
|
|
|
}
|