From d284f48496a5b32f687a233f1e09d4b66d911b22 Mon Sep 17 00:00:00 2001 From: blazSmehov Date: Tue, 2 Dec 2025 23:16:48 +0100 Subject: [PATCH] feat: add websocket and dockerfile to build location --- build/package/Dockerfile.location | 17 ++++ cmd/server/main.go | 77 ++++++++++++++++++- internal/pkg/common/appcontext/context.go | 6 ++ internal/pkg/controller/beacons_controller.go | 4 +- 4 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 build/package/Dockerfile.location diff --git a/build/package/Dockerfile.location b/build/package/Dockerfile.location new file mode 100644 index 0000000..20a63c3 --- /dev/null +++ b/build/package/Dockerfile.location @@ -0,0 +1,17 @@ +# syntax=docker/dockerfile:1 + +FROM golang:1.24.0 AS builder +WORKDIR /app + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o location ./cmd/location + +FROM alpine:latest +RUN apk add --no-cache ca-certificates +WORKDIR /app +COPY --from=builder /app/location . + +ENTRYPOINT ["./location"] diff --git a/cmd/server/main.go b/cmd/server/main.go index 1e0d148..dec9bdc 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -2,11 +2,14 @@ package main import ( "context" + "encoding/json" "fmt" + "log" "net/http" "os/signal" "sync" "syscall" + "time" "github.com/AFASystems/presence/internal/pkg/common/appcontext" "github.com/AFASystems/presence/internal/pkg/config" @@ -16,8 +19,14 @@ import ( "github.com/AFASystems/presence/internal/pkg/service" "github.com/gorilla/handlers" "github.com/gorilla/mux" + "github.com/gorilla/websocket" ) +var upgrader = websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, +} + var wg sync.WaitGroup func main() { @@ -51,7 +60,7 @@ func main() { r := mux.NewRouter() // For now just add beacon DELETE / GET / POST / PUT methods - r.HandleFunc("/api/beacons/{beacon_id}", controller.BeaconsDeleteController(writer, ctx)).Methods("DELETE") + r.HandleFunc("/api/beacons/{beacon_id}", controller.BeaconsDeleteController(writer, ctx, appState)).Methods("DELETE") r.HandleFunc("/api/beacons", controller.BeaconsListController(appState)).Methods("GET") r.HandleFunc("/api/beacons/{beacon_id}", controller.BeaconsListSingleController(appState)).Methods("GET") r.HandleFunc("/api/beacons", controller.BeaconsAddController(writer, ctx)).Methods("POST") @@ -60,6 +69,8 @@ func main() { r.HandleFunc("/api/settings", controller.SettingsListController(appState, client, ctx)).Methods("GET") r.HandleFunc("/api/settings", controller.SettingsEditController(settingsWriter, appState, client, ctx)).Methods("POST") + r.HandleFunc("/api/beacons/ws", serveWs(appState, ctx)) + http.ListenAndServe(cfg.HTTPAddr, handlers.CORS(originsOk, headersOk, methodsOk)(r)) eventLoop: @@ -87,3 +98,67 @@ eventLoop: fmt.Println("All kafka clients shutdown, starting shutdown of valkey client") appState.CleanValkeyClient() } + +func serveWs(appstate *appcontext.AppState, ctx context.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ws, err := upgrader.Upgrade(w, r, nil) + if err != nil { + if _, ok := err.(websocket.HandshakeError); !ok { + log.Println(err) + } + return + } + wg.Add(1) + go writer(ws, appstate, ctx) + reader(ws) + } +} + +func writer(ws *websocket.Conn, appstate *appcontext.AppState, ctx context.Context) { + pingTicker := time.NewTicker((60 * 9) / 10 * time.Second) + beaconTicker := time.NewTicker(2 * time.Second) + defer func() { + pingTicker.Stop() + beaconTicker.Stop() + ws.Close() + wg.Done() + }() + for { + select { + case <-ctx.Done(): + log.Println("WebSocket writer received shutdown signal.") + ws.SetWriteDeadline(time.Now().Add(10 * time.Second)) + ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) + return + case <-beaconTicker.C: + beacons := appstate.GetAllBeacons() + js, err := json.Marshal(beacons) + if err != nil { + js = []byte("error") + } + + ws.SetWriteDeadline(time.Now().Add(10 * time.Second)) + if err := ws.WriteMessage(websocket.TextMessage, js); err != nil { + return + } + case <-pingTicker.C: + ws.SetWriteDeadline(time.Now().Add(10 * time.Second)) + if err := ws.WriteMessage(websocket.PingMessage, []byte{}); err != nil { + return + } + } + } +} + +func reader(ws *websocket.Conn) { + defer ws.Close() + ws.SetReadLimit(512) + ws.SetReadDeadline(time.Now().Add((60 * 9) / 10 * time.Second)) + ws.SetPongHandler(func(string) error { ws.SetReadDeadline(time.Now().Add((60 * 9) / 10 * time.Second)); return nil }) + for { + _, _, err := ws.ReadMessage() + if err != nil { + break + } + } +} diff --git a/internal/pkg/common/appcontext/context.go b/internal/pkg/common/appcontext/context.go index f60774e..2fe3c4c 100644 --- a/internal/pkg/common/appcontext/context.go +++ b/internal/pkg/common/appcontext/context.go @@ -167,6 +167,12 @@ func (m *AppState) RemoveBeaconFromLookup(id string) { delete(m.beaconsLookup, id) } +func (m *AppState) RemoveBeacon(id string) { + m.beacons.Lock.Lock() + delete(m.beacons.Beacons, id) + m.beacons.Lock.Unlock() +} + // BeaconExists checks if a beacon exists in the lookup func (m *AppState) BeaconExists(id string) bool { _, exists := m.beaconsLookup[id] diff --git a/internal/pkg/controller/beacons_controller.go b/internal/pkg/controller/beacons_controller.go index e092407..77fbf1a 100644 --- a/internal/pkg/controller/beacons_controller.go +++ b/internal/pkg/controller/beacons_controller.go @@ -59,7 +59,7 @@ func sendKafkaMessage(writer *kafka.Writer, value *model.ApiUpdate, ctx context. return nil } -func BeaconsDeleteController(writer *kafka.Writer, ctx context.Context) http.HandlerFunc { +func BeaconsDeleteController(writer *kafka.Writer, ctx context.Context, appstate *appcontext.AppState) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) beaconId := vars["beacon_id"] @@ -76,6 +76,8 @@ func BeaconsDeleteController(writer *kafka.Writer, ctx context.Context) http.Han return } + // If message is succesfully sent delete the beacon from the list + appstate.RemoveBeacon(beaconId) w.Write([]byte("ok")) } }