// tera-sessiond owns Direct Mode WebSocket attachments and bounded fan-out. package main import ( "context" "encoding/json" "log" "net/http" "os" "os/signal" "strings" "sync" "syscall" "time" "tera-clone/services/tera-api/internal/directstream" "tera-clone/services/tera-api/internal/serial" ) func main() { listenAddress := sessiondGetenv("TERA_SESSIOND_LISTEN_ADDR", "127.0.0.1:8081") ticketSockets := sessiondGetenvList("TERA_SESSIOND_TICKET_SOCKETS") if len(ticketSockets) == 0 { ticketSockets = []string{sessiondGetenv("TERA_SESSIOND_TICKET_SOCKET", directstream.DefaultTicketSocketPath)} } serialSocket := sessiondGetenv("TERA_SESSIOND_SERIAL_STREAM_SOCKET", serial.DefaultStreamSocketPath) ticketClient := directstream.NewTicketSocketPool(ticketSockets) transportFactory := serial.NewStreamSocketClient(serialSocket, "tera-sessiond") broker, err := directstream.NewBroker(transportFactory, directstream.BrokerOptions{}) if err != nil { log.Fatalf("tera-sessiond broker setup failed: %v", err) } defer broker.Close() webSocketMetrics := &directstream.WebSocketMetrics{} streamHandler := directstream.NewWebSocketHandler(broker, ticketClient, ticketClient, directstream.WebSocketOptions{ AllowedOrigins: sessiondGetenvList("TERA_SESSIOND_ALLOWED_ORIGINS"), Metrics: webSocketMetrics, }) mux := http.NewServeMux() mux.Handle("/api/v1/direct/", streamHandler) statusHandler := newSessiondStatusHandler(broker, webSocketMetrics, ticketClient, transportFactory) mux.Handle("/healthz", statusHandler(false)) mux.Handle("/readyz", statusHandler(true)) server := &http.Server{ Addr: listenAddress, Handler: mux, ReadHeaderTimeout: 5 * time.Second, IdleTimeout: 60 * time.Second, } log.Printf("tera-sessiond listening on http://%s", listenAddress) serverErrors := make(chan error, 1) go func() { serverErrors <- server.ListenAndServe() }() signalContext, stopSignals := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stopSignals() select { case <-signalContext.Done(): log.Printf("tera-sessiond stopping") _ = broker.Close() shutdownContext, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := server.Shutdown(shutdownContext); err != nil { log.Printf("tera-sessiond HTTP shutdown failed: %v", err) } case err := <-serverErrors: if err != nil && err != http.ErrServerClosed { log.Fatalf("tera-sessiond failed: %v", err) } } } type ticketStatusProvider interface { Status(context.Context) (directstream.TicketServiceSnapshot, error) } type serialStatusProvider interface { Status(context.Context) (serial.StreamSnapshot, error) } type sessiondStatus struct { Status string `json:"status"` Broker directstream.BrokerSnapshot `json:"broker"` WebSocket directstream.WebSocketSnapshot `json:"websocket"` Tickets directstream.TicketServiceSnapshot `json:"tickets"` Serial serial.StreamSnapshot `json:"serial"` TicketError string `json:"ticket_error,omitempty"` SerialError string `json:"serial_error,omitempty"` } func newSessiondStatusHandler(broker *directstream.Broker, metrics *directstream.WebSocketMetrics, tickets ticketStatusProvider, seriald serialStatusProvider) func(bool) http.Handler { return func(readiness bool) http.Handler { return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) { if request.Method != http.MethodGet { response.Header().Set("Allow", http.MethodGet) response.WriteHeader(http.StatusMethodNotAllowed) return } ctx, cancel := context.WithTimeout(request.Context(), time.Second) defer cancel() status := sessiondStatus{Status: "ok", Broker: broker.Snapshot(), WebSocket: metrics.Snapshot()} var wait sync.WaitGroup wait.Add(2) go func() { defer wait.Done() value, err := tickets.Status(ctx) status.Tickets = value if err != nil { status.TicketError = err.Error() } }() go func() { defer wait.Done() value, err := seriald.Status(ctx) status.Serial = value if err != nil { status.SerialError = err.Error() } }() wait.Wait() if status.TicketError != "" || status.SerialError != "" { status.Status = "degraded" } response.Header().Set("Content-Type", "application/json") if readiness && status.Status != "ok" { response.WriteHeader(http.StatusServiceUnavailable) } _ = json.NewEncoder(response).Encode(status) }) } } func sessiondGetenv(key, fallback string) string { if value := strings.TrimSpace(os.Getenv(key)); value != "" { return value } return fallback } func sessiondGetenvList(key string) []string { value := os.Getenv(key) result := make([]string, 0) for _, item := range strings.Split(value, ",") { item = strings.TrimSpace(item) if item != "" { result = append(result, item) } } return result }