Files
device_agent/main.go
T
2026-05-30 17:28:38 +02:00

147 lines
3.7 KiB
Go

package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"log"
"net"
"net/http"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"
)
const Version = "0.2.0"
func main() {
listen := flag.String("listen", "127.0.0.1:7474", "host:port a escuchar (ej. 10.42.0.10:7474 para mesh wg1)")
manifestPath := flag.String("manifest", defaultManifestPath(), "ruta al manifest YAML")
auditPath := flag.String("audit", "local_files/audit.db", "ruta al audit.db sqlite")
selfTest := flag.Bool("self-test", false, "smoke check, sale 0 si todo OK")
flag.Parse()
if *selfTest {
fmt.Println("device_agent self-test OK")
os.Exit(0)
}
// Cargar manifest.
mf, err := LoadManifest(*manifestPath)
if err != nil {
log.Fatalf("manifest load %s: %v", *manifestPath, err)
}
log.Printf("manifest loaded device_id=%s capabilities=%d", mf.DeviceID, len(mf.Capabilities))
// Abrir audit DB.
_ = os.MkdirAll(filepath.Dir(*auditPath), 0700)
audit, err := OpenAudit(*auditPath)
if err != nil {
log.Fatalf("audit open %s: %v", *auditPath, err)
}
defer audit.Close()
log.Printf("audit db opened %s", *auditPath)
// Construir router.
mux := http.NewServeMux()
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{
"ok": true,
"device_id": mf.DeviceID,
"version": Version,
"ts": time.Now().Unix(),
})
})
mux.HandleFunc("/capabilities", func(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]any{
"device_id": mf.DeviceID,
"capabilities": mf.Capabilities,
})
})
mux.HandleFunc("/capability", capabilityHandler(mf, audit))
srv := &http.Server{
Addr: *listen,
Handler: loggingMiddleware(mux),
ReadHeaderTimeout: 5 * time.Second,
}
// Listen explicit para detectar bind issues claro.
ln, err := net.Listen("tcp", *listen)
if err != nil {
log.Fatalf("listen %s: %v", *listen, err)
}
log.Printf("device_agent v%s listening %s (device_id=%s)", Version, *listen, mf.DeviceID)
// Graceful shutdown.
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
go func() {
if err := srv.Serve(ln); err != nil && err != http.ErrServerClosed {
log.Fatalf("serve: %v", err)
}
}()
<-ctx.Done()
log.Println("shutdown signal received, draining...")
shutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = srv.Shutdown(shutCtx)
log.Println("bye")
}
func defaultManifestPath() string {
home, _ := os.UserHomeDir()
return filepath.Join(home, ".config", "device_agent", "manifest.yaml")
}
func writeJSON(w http.ResponseWriter, status int, body any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(body)
}
func writeError(w http.ResponseWriter, status int, code, msg string) {
writeJSON(w, status, map[string]any{
"ok": false,
"error": code,
"msg": msg,
})
}
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
ww := &statusWriter{ResponseWriter: w, status: 200}
next.ServeHTTP(ww, r)
dur := time.Since(start).Milliseconds()
log.Printf("%s %s %d %dms from=%s", r.Method, r.URL.Path, ww.status, dur, clientIP(r))
})
}
type statusWriter struct {
http.ResponseWriter
status int
}
func (s *statusWriter) WriteHeader(code int) {
s.status = code
s.ResponseWriter.WriteHeader(code)
}
func clientIP(r *http.Request) string {
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return r.RemoteAddr
}
if strings.HasPrefix(host, "::") {
return host
}
return host
}