Files

170 lines
4.2 KiB
Go

package main
import (
"context"
"fmt"
"io"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"git.i3omb.com/gronod/ha-n95-local-control/internal/config"
"git.i3omb.com/gronod/ha-n95-local-control/internal/ctl"
"git.i3omb.com/gronod/ha-n95-local-control/internal/ha"
"git.i3omb.com/gronod/ha-n95-local-control/internal/httpx"
"git.i3omb.com/gronod/ha-n95-local-control/internal/mqttbridge"
"git.i3omb.com/gronod/ha-n95-local-control/internal/robot"
"git.i3omb.com/gronod/ha-n95-local-control/internal/session"
"git.i3omb.com/gronod/ha-n95-local-control/internal/xmpp"
)
var (
// shutdownBridge is assigned by phase 04.
shutdownBridge func(context.Context) error
// shutdownXMPP is assigned by phase 02.
shutdownXMPP func(context.Context) error
// shutdownTimeout is the graceful shutdown budget.
shutdownTimeout = 5 * time.Second
// logOutput is the destination for slog TextHandler.
logOutput io.Writer = os.Stderr
)
func main() {
if len(os.Args) == 2 && os.Args[1] == "healthcheck" {
os.Exit(healthcheck())
}
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
if err := run(ctx, os.Environ()); err != nil {
os.Exit(1)
}
}
func run(ctx context.Context, environ []string) error {
cfg, err := config.Load(environ)
if err != nil {
slog.Error("invalid configuration", "err", err)
return err
}
slog.SetDefault(slog.New(slog.NewTextHandler(logOutput, &slog.HandlerOptions{
Level: cfg.LogLevel,
})))
slog.Info("starting with config", "config", cfg)
groupCtx, cancel := context.WithCancel(context.Background())
defer cancel()
registry := session.NewRegistry()
bus := session.NewBus()
// Create late-bound sinks to break the circular dependency.
var bridge *mqttbridge.Bridge
diagSink := func(d session.Diagnostic) {
if bridge != nil {
bridge.DiagnosticSink()(d)
}
}
traceSink := func(serial string, tr ctl.Trace) {
if bridge != nil {
bridge.TraceSink()(serial, tr)
}
}
fleet := robot.NewFleet(groupCtx, cfg.ControllerJID, nil, traceSink, diagSink, nil)
var errBridge error
bridge, errBridge = mqttbridge.New(groupCtx, cfg, fleet.Submit)
if errBridge != nil {
return errBridge
}
robot.ParseSchedules = robot.ScheduleParserHook
robot.ParseLifespan = robot.LifespanParserHook
bridge.SendCommand = ha.SendCommandHook(cfg)
fleet.SetRepublishFactory(bridge.Republisher)
fleet.SetSendCommandFunc(bridge.DispatchSendCommand)
bus.Register(fleet)
bus.Register(bridge)
if shutdownBridge == nil {
shutdownBridge = bridge.Shutdown
}
xmppServer := xmpp.NewServer(cfg, registry, bus, nil, diagSink)
if shutdownXMPP == nil {
shutdownXMPP = xmppServer.Shutdown
}
errCh := make(chan error, 4)
go func() { errCh <- httpx.ServeLookup(groupCtx, cfg) }()
go func() { errCh <- httpx.ServeFirmware(groupCtx, cfg) }()
go func() { errCh <- httpx.ServeHealth(groupCtx, cfg) }()
go func() { errCh <- xmppServer.Serve(groupCtx) }()
var runErr error
select {
case runErr = <-errCh:
cancel()
case <-ctx.Done():
}
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), shutdownTimeout)
defer shutdownCancel()
if shutdownBridge != nil {
_ = shutdownBridge(shutdownCtx)
}
if shutdownXMPP != nil {
_ = shutdownXMPP(shutdownCtx)
}
cancel()
for i := 0; i < 4; i++ {
if err := <-errCh; err != nil && runErr == nil {
runErr = err
}
}
if shutdownCtx.Err() != nil {
slog.Info("shutdown budget expired")
return nil
}
if runErr != nil {
slog.Error("listener failed", "err", runErr)
}
return runErr
}
func healthcheck() int {
cfg, err := config.Load(os.Environ())
if err != nil {
slog.Error("invalid configuration", "err", err)
return 1
}
url := fmt.Sprintf("http://127.0.0.1:%d/healthz", cfg.HealthPort)
client := &http.Client{Timeout: 2 * time.Second}
resp, err := client.Get(url)
if err != nil {
slog.Error("healthcheck request failed", "err", err)
return 1
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
slog.Error("healthcheck body read failed", "err", err)
return 1
}
if resp.StatusCode != http.StatusOK || string(body) != "ok\n" {
slog.Error("healthcheck failed", "status", resp.StatusCode, "body", string(body))
return 1
}
return 0
}