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...) }