This commit is contained in:
2026-10-11 14:59:51 +07:00
parent e3576db693
commit 5da08f6916
17 changed files with 1235 additions and 258 deletions
+88
View File
@@ -0,0 +1,88 @@
package main
import (
"context"
"encoding/json"
"log"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"
"evening_detective_server/internal/modules/cudy"
"evening_detective_server/internal/services/wifi_service"
)
func collectCudy() {
path := os.Getenv("CUDY_SNAPSHOT_FILE")
if path == "" {
log.Fatal("CUDY_SNAPSHOT_FILE is required")
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
snapshot := cudy.Snapshot{}
var reader cudy.Reader
passwordFile := os.Getenv("CUDY_PASSWORD_FILE")
if passwordFile != "" {
password, err := os.ReadFile(passwordFile)
if err != nil {
log.Fatal("cannot read Cudy credential file")
}
base := os.Getenv("CUDY_URL")
if base == "" {
base = "http://192.168.10.1"
}
reader, err = cudy.NewReader(base, strings.TrimRight(string(password), "\r\n"))
if err != nil {
log.Fatal("invalid Cudy configuration")
}
snapshot, _ = wifi_service.New(path).Get(ctx)
snapshot.Enabled = true
}
timer := time.NewTicker(10 * time.Second)
defer timer.Stop()
for {
if reader != nil {
clients, err := reader.Read(ctx)
snapshot.HasError = err != nil
if err == nil {
snapshot.Clients = clients
snapshot.CollectedAt = time.Now().UTC()
}
}
if err := writeCudySnapshot(path, snapshot); err != nil {
log.Print("cannot publish Cudy snapshot")
}
select {
case <-ctx.Done():
return
case <-timer.C:
}
}
}
func writeCudySnapshot(path string, snapshot cudy.Snapshot) error {
data, err := json.Marshal(snapshot)
if err != nil {
return err
}
file, err := os.CreateTemp(filepath.Dir(path), ".cudy-")
if err != nil {
return err
}
name := file.Name()
defer os.Remove(name)
if err = file.Chmod(0644); err == nil {
_, err = file.Write(data)
}
closeErr := file.Close()
if err != nil {
return err
}
if closeErr != nil {
return closeErr
}
return os.Rename(name, path)
}
+34
View File
@@ -0,0 +1,34 @@
package main
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"time"
"evening_detective_server/internal/modules/cudy"
)
func TestAtomicCudySnapshot(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "state.json")
for _, failed := range []bool{false, true} {
snapshot := cudy.Snapshot{Enabled: true, CollectedAt: time.Unix(123, 0), HasError: failed, Clients: []cudy.Client{{MAC: "AA:BB:CC:DD:EE:01"}}}
if err := writeCudySnapshot(path, snapshot); err != nil {
t.Fatal(err)
}
data, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
var result cudy.Snapshot
if json.Unmarshal(data, &result) != nil || result.HasError != failed || len(result.Clients) != 1 {
t.Fatal("snapshot corrupted")
}
}
files, err := os.ReadDir(dir)
if err != nil || len(files) != 1 {
t.Fatal("temporary snapshots left behind")
}
}
+6
View File
@@ -56,6 +56,7 @@ import (
"evening_detective_server/internal/services/ticket_service"
"evening_detective_server/internal/services/ui_service"
"evening_detective_server/internal/services/users_service"
"evening_detective_server/internal/services/wifi_service"
proto "evening_detective_server/proto"
"fmt"
"log"
@@ -89,6 +90,10 @@ const grpcMsgLimit = 257 << 20
func main() {
_ = godotenv.Load()
if len(os.Args) == 2 && os.Args[1] == "--collect-cudy" {
collectCudy()
return
}
mode := "prod"
if os.Getenv("APP_MODE") == "venue" {
mode = "venue"
@@ -292,6 +297,7 @@ func main() {
gameResultsService,
gameResultsApplyService,
devicePresenceService,
wifi_service.New(os.Getenv("CUDY_SNAPSHOT_FILE")),
),
)
@@ -5056,6 +5056,10 @@
"serverTime": {
"type": "string",
"format": "date-time"
},
"wifi": {
"$ref": "#/definitions/evening_detective_serverVenueWiFi",
"description": "Entire venue, independent of the game filter."
}
}
},
@@ -6346,6 +6350,50 @@
}
}
},
"evening_detective_serverVenueWiFi": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean"
},
"collectedAt": {
"type": "string",
"format": "date-time"
},
"stale": {
"type": "boolean"
},
"hasError": {
"type": "boolean"
},
"clients": {
"type": "array",
"items": {
"type": "object",
"$ref": "#/definitions/evening_detective_serverWiFiClient"
}
}
}
},
"evening_detective_serverWiFiClient": {
"type": "object",
"properties": {
"IP": {
"type": "string"
},
"MAC": {
"type": "string"
},
"band": {
"type": "string"
},
"signal": {
"type": "integer",
"format": "int32",
"description": "dBm; absent when the router does not report it."
}
}
},
"protobufAny": {
"type": "object",
"properties": {