Files
yovision/Sense/cmd/sense-api/main.go
T

114 lines
3.0 KiB
Go
Raw Normal View History

package main
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"sync"
"syscall"
"time"
"yovision/sense/internal/config"
"yovision/sense/internal/mtx"
"yovision/sense/internal/onvif"
"yovision/sense/internal/probe"
"yovision/sense/internal/reconcile"
"yovision/sense/internal/store"
)
var version = "dev"
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
if err := run(logger); err != nil {
logger.Error("Sense stopped", "error", err)
os.Exit(1)
}
}
func run(logger *slog.Logger) error {
cfg, err := config.Load()
if err != nil {
return fmt.Errorf("load configuration: %w", err)
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
repository, err := store.OpenSQLite(ctx, cfg.DatabaseDSN)
if err != nil {
return err
}
defer repository.Close()
mediaClient, err := mtx.NewClient(cfg.MediaMTXURL, nil)
if err != nil {
return err
}
// T-003 deliberately has no real camera adapter. T-006 replaces this port
// only after the device whitelist and five-camera evidence are available.
reconciler := reconcile.New(repository, onvif.UnavailableAdapter{}, mediaClient)
checker := probe.New(repository, mediaClient)
report := func(err error) {
// Domain and MediaMTX errors intentionally omit stream URIs and credentials.
logger.Warn("background convergence error", "error", err)
}
var background sync.WaitGroup
background.Add(2)
go func() {
defer background.Done()
reconciler.Run(ctx, cfg.ReconcileInterval, report)
}()
go func() {
defer background.Done()
checker.Run(ctx, cfg.ProbeInterval, report)
}()
mux := http.NewServeMux()
mux.HandleFunc("GET /healthz", func(writer http.ResponseWriter, _ *http.Request) {
writer.Header().Set("Content-Type", "application/json")
writer.WriteHeader(http.StatusOK)
_, _ = writer.Write([]byte(`{"status":"ok"}`))
})
mux.HandleFunc("GET /readyz", func(writer http.ResponseWriter, _ *http.Request) {
writer.Header().Set("Content-Type", "application/json")
writer.WriteHeader(http.StatusOK)
_, _ = writer.Write([]byte(`{"status":"ready"}`))
})
server := &http.Server{
Addr: cfg.HTTPAddress, Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
serverErrors := make(chan error, 1)
go func() {
logger.Info("Sense listening", "address", cfg.HTTPAddress, "version", version)
serverErrors <- server.ListenAndServe()
}()
select {
case <-ctx.Done():
case serverErr := <-serverErrors:
if !errors.Is(serverErr, http.ErrServerClosed) {
stop()
background.Wait()
return fmt.Errorf("serve HTTP: %w", serverErr)
}
}
stop()
shutdownContext, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
shutdownErr := server.Shutdown(shutdownContext)
background.Wait()
if shutdownErr != nil {
return fmt.Errorf("shutdown HTTP server: %w", shutdownErr)
}
return nil
}