169 lines
5.3 KiB
Go
169 lines
5.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"net"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"modelRT/client/manualsync"
|
|
)
|
|
|
|
type listenFunc func(network, address string) (net.Listener, error)
|
|
|
|
const (
|
|
cl3611Address = ":9001"
|
|
protocol104Address = ":9002"
|
|
manualAPIPath = "/api/manual"
|
|
healthPath = "/healthz"
|
|
maxRequestBody = 1 << 20
|
|
shutdownTimeout = 5 * time.Second
|
|
)
|
|
|
|
func newProtocolServer(protocol, address string, logger *slog.Logger) *http.Server {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("POST "+manualAPIPath, manualHandler(protocol, strings.TrimPrefix(address, ":"), logger))
|
|
mux.HandleFunc("GET "+healthPath, healthHandler)
|
|
|
|
return &http.Server{
|
|
Addr: address,
|
|
Handler: mux,
|
|
ReadHeaderTimeout: 5 * time.Second,
|
|
ReadTimeout: 10 * time.Second,
|
|
WriteTimeout: 10 * time.Second,
|
|
IdleTimeout: 60 * time.Second,
|
|
}
|
|
}
|
|
|
|
func run(ctx context.Context, logger *slog.Logger, listen listenFunc) error {
|
|
cl3611Listener, err := listen("tcp", cl3611Address)
|
|
if err != nil {
|
|
return fmt.Errorf("listen on %s: %w", cl3611Address, err)
|
|
}
|
|
protocol104Listener, err := listen("tcp", protocol104Address)
|
|
if err != nil {
|
|
_ = cl3611Listener.Close()
|
|
return fmt.Errorf("listen on %s: %w", protocol104Address, err)
|
|
}
|
|
|
|
servers := []struct {
|
|
protocol string
|
|
server *http.Server
|
|
listener net.Listener
|
|
}{
|
|
{protocol: "cl3611", server: newProtocolServer("cl3611", cl3611Address, logger), listener: cl3611Listener},
|
|
{protocol: "104", server: newProtocolServer("104", protocol104Address, logger), listener: protocol104Listener},
|
|
}
|
|
|
|
serveErrors := make(chan error, len(servers))
|
|
var serversWaitGroup sync.WaitGroup
|
|
for _, configuredServer := range servers {
|
|
serversWaitGroup.Add(1)
|
|
go func() {
|
|
defer serversWaitGroup.Done()
|
|
logger.Info("manual sync mock server started",
|
|
"protocol", configuredServer.protocol,
|
|
"listen_addr", configuredServer.listener.Addr().String(),
|
|
)
|
|
if err := configuredServer.server.Serve(configuredServer.listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
serveErrors <- fmt.Errorf("%s server failed: %w", configuredServer.protocol, err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
var runError error
|
|
select {
|
|
case <-ctx.Done():
|
|
case runError = <-serveErrors:
|
|
}
|
|
|
|
shutdownContext, cancelShutdown := context.WithTimeout(context.Background(), shutdownTimeout)
|
|
defer cancelShutdown()
|
|
for _, configuredServer := range servers {
|
|
if err := configuredServer.server.Shutdown(shutdownContext); err != nil {
|
|
runError = errors.Join(runError, fmt.Errorf("shut down %s server: %w", configuredServer.protocol, err))
|
|
_ = configuredServer.server.Close()
|
|
}
|
|
}
|
|
serversWaitGroup.Wait()
|
|
return runError
|
|
}
|
|
|
|
func healthHandler(response http.ResponseWriter, _ *http.Request) {
|
|
response.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
response.WriteHeader(http.StatusOK)
|
|
_, _ = response.Write([]byte("ok\n"))
|
|
}
|
|
|
|
func manualHandler(protocol, listenPort string, logger *slog.Logger) http.HandlerFunc {
|
|
return func(response http.ResponseWriter, request *http.Request) {
|
|
request.Body = http.MaxBytesReader(response, request.Body, maxRequestBody)
|
|
var payload manualsync.Request
|
|
decoder := json.NewDecoder(request.Body)
|
|
if err := decoder.Decode(&payload); err != nil {
|
|
logger.LogAttrs(request.Context(), slog.LevelWarn, "invalid manual sync request",
|
|
slog.String("protocol", protocol),
|
|
slog.String("listen_port", listenPort),
|
|
slog.String("remote_addr", request.RemoteAddr),
|
|
slog.Any("error", err),
|
|
)
|
|
var maxBytesError *http.MaxBytesError
|
|
if errors.As(err, &maxBytesError) {
|
|
http.Error(response, "request body too large", http.StatusRequestEntityTooLarge)
|
|
return
|
|
}
|
|
http.Error(response, "invalid JSON request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
var trailing any
|
|
if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) {
|
|
if err == nil {
|
|
err = errors.New("request body contains multiple JSON documents")
|
|
}
|
|
logger.LogAttrs(request.Context(), slog.LevelWarn, "invalid manual sync request",
|
|
slog.String("protocol", protocol),
|
|
slog.String("listen_port", listenPort),
|
|
slog.String("remote_addr", request.RemoteAddr),
|
|
slog.Any("error", err),
|
|
)
|
|
http.Error(response, "invalid JSON request body", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
logger.LogAttrs(request.Context(), slog.LevelInfo, "manual sync request received",
|
|
slog.String("protocol", protocol),
|
|
slog.String("listen_port", listenPort),
|
|
slog.String("remote_addr", request.RemoteAddr),
|
|
slog.Int("mode", int(payload.Mode)),
|
|
slog.Group("target",
|
|
slog.Int("type", payload.Target.Type),
|
|
slog.String("station", payload.Target.Station),
|
|
slog.String("main_pos", payload.Target.MainPos),
|
|
slog.String("sub_pos", payload.Target.SubPos),
|
|
slog.String("option", payload.Target.Option),
|
|
),
|
|
slog.Attr{Key: "data", Value: samplesLogValue(payload.Data)},
|
|
)
|
|
response.WriteHeader(http.StatusNoContent)
|
|
}
|
|
}
|
|
|
|
func samplesLogValue(samples []manualsync.SyntheticData) slog.Value {
|
|
attributes := make([]slog.Attr, 0, len(samples))
|
|
for index, sample := range samples {
|
|
attributes = append(attributes, slog.Group(strconv.Itoa(index),
|
|
slog.Int64("time", sample.Time),
|
|
slog.Float64("value", sample.Value),
|
|
))
|
|
}
|
|
return slog.GroupValue(attributes...)
|
|
}
|