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.
113 lines
2.5 KiB
Go
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)))
|
|
}
|