Files
gronod 7bed99cc4c fix: keep robot listeners if health port is busy (v0.1.2)
ServeHealth bind failures (common on HAOS when OTBR owns 8080) are
logged and ignored. Lookup, firmware, and XMPP stay up. Log each
successful listen address so add-on logs show the sockets.
2026-09-24 16:17:30 +00:00

113 lines
2.5 KiB
Go

package xmpp
import (
"context"
"fmt"
"log/slog"
"net"
"time"
"git.i3omb.com/gronod/ha-n95-local-control/internal/config"
"git.i3omb.com/gronod/ha-n95-local-control/internal/session"
)
// Clock abstracts time for the ping loop so tests can advance the clock without
// sleeping.
type Clock interface {
Now() time.Time
After(d time.Duration) <-chan time.Time
}
type realClock struct{}
func (realClock) Now() time.Time { return time.Now() }
func (realClock) After(d time.Duration) <-chan time.Time { return time.After(d) }
// Server holds the runtime dependencies for the XMPP listener.
type Server struct {
cfg config.Config
registry *session.Registry
bus *session.Bus
clock Clock
diag session.DiagnosticSink
ln net.Listener
}
// NewServer creates a server. If clock is nil, real time is used.
func NewServer(cfg config.Config, registry *session.Registry, bus *session.Bus, clock Clock, diag session.DiagnosticSink) *Server {
if clock == nil {
clock = realClock{}
}
return &Server{
cfg: cfg,
registry: registry,
bus: bus,
clock: clock,
diag: diag,
}
}
// Serve starts the plaintext XMPP listener and blocks until ctx is cancelled.
// It returns nil on graceful shutdown and the listen error on failure.
func (s *Server) Serve(ctx context.Context) error {
addr := net.JoinHostPort(s.cfg.BindAddress, fmt.Sprintf("%d", s.cfg.PortXMPP))
ln, err := net.Listen("tcp", addr)
if err != nil {
return fmt.Errorf("%s: %w", addr, err)
}
s.ln = ln
slog.Info("xmpp listening", "addr", addr)
errCh := make(chan error, 1)
go func() {
errCh <- s.acceptLoop(ctx)
}()
select {
case <-ctx.Done():
_ = ln.Close()
return nil
case err := <-errCh:
return err
}
}
func (s *Server) acceptLoop(ctx context.Context) error {
for {
conn, err := s.ln.Accept()
if err != nil {
if ctx.Err() != nil {
return nil
}
return err
}
c := &Conn{
netConn: conn,
cfg: s.cfg,
registry: s.registry,
bus: s.bus,
clock: s.clock,
diag: s.diag,
}
go c.serve()
}
}
// Shutdown writes </stream:stream> best-effort on every current generation and
// closes the underlying sockets. It is safe to call on a nil Server.
func (s *Server) Shutdown(ctx context.Context) error {
if s == nil || s.registry == nil {
return nil
}
s.registry.CloseAll()
if s.ln != nil {
_ = s.ln.Close()
}
return nil
}
// logStanza logs a redacted copy of a stanza at debug level.
func logStanza(dir string, b []byte) {
slog.Debug("xmpp", "dir", dir, "stanza", string(Redact(b)))
}