// Package ipc defines the tiny control protocol shared by the daemon (server) // and the CLI/TUI (clients). Messages are newline-delimited JSON over a unix // socket, so switching layouts or reading status never requires restarting // the video wall. package ipc import ( "bufio" "encoding/json" "fmt" "net" "os" "path/filepath" "strconv" "time" ) // Request is a command sent to the daemon. type Request struct { Cmd string `json:"cmd"` // "status" | "reload" | "set-layout" Name string `json:"name,omitempty"` // layout name for set-layout } // SlotStatus reports one grid cell's state. type SlotStatus struct { Slot int `json:"slot"` Camera string `json:"camera"` PID int `json:"pid"` Running bool `json:"running"` Healthy bool `json:"healthy"` } // Response is the daemon's reply. type Response struct { OK bool `json:"ok"` Error string `json:"error,omitempty"` ActiveLayout string `json:"active_layout,omitempty"` Slots []SlotStatus `json:"slots,omitempty"` } // SocketPath returns the control socket path inside the runtime dir. func SocketPath() string { return filepath.Join(RunDir(), "control.sock") } // RunDir is the per-user runtime directory for sockets, created on demand. func RunDir() string { base := os.Getenv("XDG_RUNTIME_DIR") if base == "" { base = filepath.Join(os.TempDir(), "rtsp-streamer-"+strconv.Itoa(os.Getuid())) } else { base = filepath.Join(base, "rtsp-streamer") } return base } // Send dials the daemon, sends one request, and returns the reply. func Send(req Request) (*Response, error) { conn, err := net.DialTimeout("unix", SocketPath(), 3*time.Second) if err != nil { return nil, fmt.Errorf("cannot reach daemon (is it running?): %w", err) } defer conn.Close() _ = conn.SetDeadline(time.Now().Add(5 * time.Second)) enc := json.NewEncoder(conn) if err := enc.Encode(req); err != nil { return nil, err } var resp Response if err := json.NewDecoder(bufio.NewReader(conn)).Decode(&resp); err != nil { return nil, err } return &resp, nil }