Files
Strix/internal/test/test.go
T
eduard256 27117900eb Rewrite Strix from scratch as single binary
Complete architecture rewrite following go2rtc patterns:
- pkg/ for pure logic (camdb, tester, probe, generate)
- internal/ for application glue with Init() modules
- Single HTTP server on :4567 with all endpoints
- zerolog with password masking and memory ring buffer
- Environment-based config only (no YAML files)

API endpoints: /api/search, /api/streams, /api/test,
/api/probe, /api/generate, /api/health, /api/log

Dependencies: go2rtc v1.9.14, go-sqlite3, miekg/dns, zerolog
2026-03-25 10:38:46 +00:00

200 lines
4.0 KiB
Go

package test
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"net/http"
"strconv"
"sync"
"time"
"github.com/eduard256/strix/internal/api"
"github.com/eduard256/strix/internal/app"
"github.com/eduard256/strix/pkg/tester"
"github.com/rs/zerolog"
)
var log zerolog.Logger
var sessions = map[string]*tester.Session{}
var sessionsMu sync.Mutex
func Init() {
log = app.GetLogger("test")
api.HandleFunc("api/test", apiTest)
api.HandleFunc("api/test/screenshot", apiScreenshot)
// cleanup expired sessions
go func() {
for {
time.Sleep(time.Minute)
sessionsMu.Lock()
for id, s := range sessions {
s.Lock()
expired := s.Status == "done" && time.Since(s.ExpiresAt) > 0
s.Unlock()
if expired {
delete(sessions, id)
}
}
sessionsMu.Unlock()
}
}()
}
func apiTest(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
id := r.URL.Query().Get("id")
if id == "" {
apiTestList(w)
return
}
apiTestGet(w, id)
case "POST":
apiTestCreate(w, r)
case "DELETE":
id := r.URL.Query().Get("id")
if id == "" {
http.Error(w, "id required", http.StatusBadRequest)
return
}
apiTestDelete(w, id)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}
func apiTestList(w http.ResponseWriter) {
type summary struct {
ID string `json:"session_id"`
Status string `json:"status"`
Total int `json:"total"`
Tested int `json:"tested"`
Alive int `json:"alive"`
WithScreen int `json:"with_screenshot"`
}
sessionsMu.Lock()
items := make([]summary, 0, len(sessions))
for _, s := range sessions {
s.Lock()
items = append(items, summary{
ID: s.ID,
Status: s.Status,
Total: s.Total,
Tested: s.Tested,
Alive: s.Alive,
WithScreen: s.WithScreen,
})
s.Unlock()
}
sessionsMu.Unlock()
api.ResponseJSON(w, map[string]any{"sessions": items})
}
func apiTestGet(w http.ResponseWriter, id string) {
sessionsMu.Lock()
s := sessions[id]
sessionsMu.Unlock()
if s == nil {
http.Error(w, "session not found", http.StatusNotFound)
return
}
s.Lock()
api.ResponseJSON(w, s)
s.Unlock()
}
func apiTestCreate(w http.ResponseWriter, r *http.Request) {
var req struct {
Sources struct {
Streams []string `json:"streams"`
} `json:"sources"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if len(req.Sources.Streams) == 0 {
http.Error(w, "sources.streams required", http.StatusBadRequest)
return
}
id := randID()
s := tester.NewSession(id, len(req.Sources.Streams))
sessionsMu.Lock()
sessions[id] = s
sessionsMu.Unlock()
log.Debug().Str("id", id).Int("urls", len(req.Sources.Streams)).Msg("[test] session created")
go tester.RunWorkers(s, req.Sources.Streams)
api.ResponseJSON(w, map[string]string{"session_id": id})
}
func apiTestDelete(w http.ResponseWriter, id string) {
sessionsMu.Lock()
if s, ok := sessions[id]; ok {
s.Cancel()
delete(sessions, id)
}
sessionsMu.Unlock()
api.ResponseJSON(w, map[string]string{"status": "deleted"})
}
func apiScreenshot(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
q := r.URL.Query()
id := q.Get("id")
idx, err := strconv.Atoi(q.Get("i"))
if id == "" || err != nil {
http.Error(w, "id and i required", http.StatusBadRequest)
return
}
sessionsMu.Lock()
s := sessions[id]
sessionsMu.Unlock()
if s == nil {
http.Error(w, "session not found", http.StatusNotFound)
return
}
data := s.GetScreenshot(idx)
if data == nil {
http.Error(w, "screenshot not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "image/jpeg")
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
w.Header().Set("Cache-Control", "no-cache")
w.Write(data)
}
func randID() string {
b := make([]byte, 8)
rand.Read(b)
return hex.EncodeToString(b)
}