// tera-seriald owns validated CH01-CH48 line configuration for TERA boards. package main import ( "context" "errors" "fmt" "log" "net" "os" "os/signal" "path/filepath" "strconv" "strings" "syscall" "time" "tera-clone/services/tera-api/internal/domain" "tera-clone/services/tera-api/internal/logservice" "tera-clone/services/tera-api/internal/logstorage" "tera-clone/services/tera-api/internal/serial" ) func main() { controlSocketPath := getenv("TERA_SERIALD_SOCKET", serial.DefaultSocketPath) streamSocketPath := getenv("TERA_SERIALD_STREAM_SOCKET", serial.DefaultStreamSocketPath) mappingPath := getenv("TERA_SERIALD_MAPPING", "/etc/tera/serial-map.json") hardwareProfile := getenv("TERA_SERIALD_HARDWARE_PROFILE", "fet3568j-c") controller, err := serial.LoadMappingController(mappingPath, hardwareProfile) if err != nil { log.Printf("tera-seriald is fail-closed: %v", err) controller = nil } if controller != nil { defer func() { if err := controller.Close(); err != nil { log.Printf("tera-seriald serial hold close failed: %v", err) } }() } controlListener, err := listen(controlSocketPath) if err != nil { log.Fatalf("tera-seriald control socket setup failed: %v", err) } streamListener, err := listen(streamSocketPath) if err != nil { _ = controlListener.Close() _ = os.Remove(controlSocketPath) log.Fatalf("tera-seriald stream socket setup failed: %v", err) } defer func() { _ = controlListener.Close() _ = streamListener.Close() _ = os.Remove(controlSocketPath) _ = os.Remove(streamSocketPath) }() unavailable := serial.UnavailableController{Reason: "validated serial mapping is not installed"} var hardware serial.Controller = unavailable var streamResolver serial.StreamResolver = unavailable if controller != nil { hardware = controller streamResolver = controller } eventSink := serial.NewBoundedStreamEventSink(getenvInt("TERA_SERIALD_EVENT_QUEUE", 1024)) streamManager, err := serial.NewStreamLeaseManager(streamResolver, serial.StreamLeaseOptions{Events: eventSink}) if err != nil { log.Fatalf("tera-seriald stream manager setup failed: %v", err) } reporterContext, stopReporter := context.WithCancel(context.Background()) defer stopReporter() var linkMonitor serial.LinkStateController = serial.DisabledLinkStateController{ Reason: "CTS/DSR/DCD are excluded from unified RX/CR connection status", } if controller != nil && getenvBool("TERA_SERIALD_LINK_SCAN_ENABLED", false) { controller.SetLinkWorkerCount(getenvInt("TERA_SERIALD_LINK_WORKERS", 8)) monitor, monitorErr := serial.NewLinkMonitor(controller, serial.LinkMonitorOptions{ Interval: time.Duration(getenvInt("TERA_SERIALD_LINK_SCAN_INTERVAL_MS", 1000)) * time.Millisecond, }) if monitorErr != nil { log.Fatalf("tera-seriald link monitor setup failed: %v", monitorErr) } linkMonitor = monitor go monitor.Run(reporterContext) } crProbeEnabled := controller != nil && getenvBool("TERA_SERIALD_CR_PROBE_ENABLED", true) var crProbeGroups []serial.CRProbeGroup if controller != nil { crProbeGroups = controller.CRProbeGroups() } crProbeManager, err := serial.NewCRProbeManager(streamManager, serial.CRProbeOptions{ Enabled: crProbeEnabled, ResponseTimeout: time.Duration(getenvInt("TERA_SERIALD_CR_PROBE_TIMEOUT_MS", 100)) * time.Millisecond, SweepInterval: time.Duration(getenvInt("TERA_SERIALD_CR_PROBE_SWEEP_SECONDS", 4)) * time.Second, BetweenChannels: time.Duration(getenvInt("TERA_SERIALD_CR_PROBE_SPACING_MS", 20)) * time.Millisecond, QuietPeriod: time.Duration(getenvInt("TERA_SERIALD_CR_PROBE_QUIET_SECONDS", 5)) * time.Second, StartupDelay: time.Duration(getenvInt("TERA_SERIALD_CR_PROBE_STARTUP_DELAY_SECONDS", 30)) * time.Second, Workers: getenvInt("TERA_SERIALD_CR_PROBE_WORKERS", 2), Groups: crProbeGroups, ConfirmResponses: getenvInt("TERA_SERIALD_CR_PROBE_CONFIRM_RESPONSES", 1), DownMisses: getenvInt("TERA_SERIALD_CR_PROBE_DOWN_MISSES", 2), }) if err != nil { log.Fatalf("tera-seriald CR probe setup failed: %v", err) } if crProbeEnabled { for _, group := range crProbeGroups { log.Printf("tera-seriald CR probe group pci_bdf=%s channels=%d", group.ID, len(group.Channels)) } } go crProbeManager.Run(reporterContext) logJournal := logservice.Journal(logservice.NewClient(getenv("TERA_SERIALD_LOG_SOCKET", logservice.DefaultSocketPath))) go reportStreamEvents(reporterContext, streamManager, eventSink.Events(), logJournal, time.Duration(getenvInt("TERA_SERIALD_REPORT_INTERVAL_SECONDS", 10))*time.Second) controlServer := serial.SocketServer{Controller: hardware, Capture: streamManager, CRProbe: crProbeManager, Links: linkMonitor} streamServer := serial.StreamSocketServer{Manager: streamManager} done := make(chan error, 2) go func() { done <- controlServer.Serve(controlListener) }() go func() { done <- streamServer.Serve(streamListener) }() log.Printf("tera-seriald serving validated serial configuration on %s", controlSocketPath) log.Printf("tera-seriald serving exclusive serial streams on %s", streamSocketPath) signals := make(chan os.Signal, 1) signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) select { case signal := <-signals: log.Printf("tera-seriald stopping after %s", signal) stopReporter() _ = controlListener.Close() _ = streamListener.Close() _ = streamManager.Close() for range 2 { if err := <-done; err != nil { log.Printf("tera-seriald stop error: %v", err) } } case err := <-done: stopReporter() _ = controlListener.Close() _ = streamListener.Close() _ = streamManager.Close() if err != nil { log.Fatalf("tera-seriald failed: %v", err) } } } func listen(socketPath string) (net.Listener, error) { if socketPath == "" || !filepath.IsAbs(socketPath) { return nil, errors.New("TERA_SERIALD_SOCKET must be an absolute path") } if err := os.MkdirAll(filepath.Dir(socketPath), 0o750); err != nil { return nil, err } if info, err := os.Lstat(socketPath); err == nil { if info.Mode()&os.ModeSocket == 0 { return nil, errors.New("refusing to replace non-socket serial control path") } if err := os.Remove(socketPath); err != nil { return nil, err } } else if !os.IsNotExist(err) { return nil, err } listener, err := net.Listen("unix", socketPath) if err != nil { return nil, err } if err := os.Chmod(socketPath, 0o660); err != nil { _ = listener.Close() return nil, err } return listener, nil } func getenv(key, fallback string) string { if value := os.Getenv(key); value != "" { return value } return fallback } func getenvInt(key string, fallback int) int { value := os.Getenv(key) if value == "" { return fallback } parsed, err := strconv.Atoi(value) if err != nil || parsed <= 0 { return fallback } return parsed } func getenvBool(key string, fallback bool) bool { value := os.Getenv(key) if value == "" { return fallback } parsed, err := strconv.ParseBool(value) if err != nil { return fallback } return parsed } type streamIntervalTotal struct { rx uint64 tx uint64 } func reportStreamEvents(ctx context.Context, manager *serial.StreamLeaseManager, events <-chan serial.StreamEvent, journal logservice.Journal, interval time.Duration) { if interval <= 0 { interval = 10 * time.Second } ticker := time.NewTicker(interval) defer ticker.Stop() totals := make(map[string]streamIntervalTotal) var storageFailures uint64 var lastStorageError error flush := func() { for key, total := range totals { if total.rx != 0 || total.tx != 0 { log.Printf("tera-seriald stream interval lease=%s rx_bytes=%d tx_bytes=%d", key, total.rx, total.tx) } delete(totals, key) } snapshot := manager.Snapshot() log.Printf("tera-seriald stream status active_leases=%d rx_bytes=%d tx_bytes=%d dropped_events=%d storage_failures=%d", snapshot.ActiveLeases, snapshot.RXBytes, snapshot.TXBytes, snapshot.DroppedEvents, storageFailures) if lastStorageError != nil { log.Printf("tera-seriald payload logging degraded: %v", lastStorageError) lastStorageError = nil } } defer flush() for { select { case event := <-events: key := event.Channel + "/" + event.LeaseID switch event.Direction { case "open", "close": log.Printf("tera-seriald stream lifecycle channel=%s lease=%s client=%s action=%s", event.Channel, event.LeaseID, event.ClientID, event.Direction) case "rx": total := totals[key] total.rx += event.Bytes totals[key] = total if err := recordPayload(journal, event); err != nil { storageFailures++ lastStorageError = err } case "tx": total := totals[key] total.tx += event.Bytes totals[key] = total if err := recordPayload(journal, event); err != nil { storageFailures++ lastStorageError = err } } case <-ticker.C: flush() case <-ctx.Done(): return } } } func recordPayload(journal logservice.Journal, event serial.StreamEvent) error { if journal == nil || len(event.Payload) == 0 { return nil } record := domain.PortLogEvent{ Timestamp: event.Timestamp, Channel: event.Channel, EventType: "traffic", Direction: strings.ToUpper(event.Direction), Actor: event.ClientID, Message: fmt.Sprintf("%s %d serial payload byte(s)", strings.ToUpper(event.Direction), len(event.Payload)), Payload: append([]byte(nil), event.Payload...), PayloadBytes: len(event.Payload), PayloadEncoding: "base64", DisplayFormat: event.LogFormat, ShowTimestamp: event.ShowTimestamp, ShowDirection: event.ShowDirection, ShowUsername: event.ShowUsername, ShowIPAddress: event.ShowIPAddress, ShowPort: event.ShowPort, } return journal.Append(logstorage.ClassPort, event.Timestamp, record) }