33package rpicamera
44
55import (
6+ "bufio"
67 "debug/elf"
8+ "errors"
79 "fmt"
810 "io"
911 "log/slog"
@@ -23,6 +25,17 @@ import (
2325 "github.com/GyeongHoKim/onvif-simulator/internal/obs"
2426)
2527
28+ // stderrTailLines bounds the post-mortem snapshot we keep of helper stderr.
29+ // libcamera prints a few dozen lines of init context plus any error detail;
30+ // 64 lines is enough to capture the tail that explains a STREAMON / IPA /
31+ // pipeline failure without unbounded memory if the helper goes haywire.
32+ const stderrTailLines = 64
33+
34+ // errTerminated is the sentinel runInner returns on a graceful Close(). It
35+ // lets run() suppress the post-mortem stderr WARN on normal shutdown without
36+ // muddling string comparison.
37+ var errTerminated = errors .New ("rpicamera: terminated" )
38+
2639const (
2740 libraryToCheckArchitecture = "libc.so.6"
2841 dumpPrefix = "/dev/shm/onvif-simulator-rpicamera-"
@@ -51,10 +64,13 @@ type Camera struct {
5164 logger * slog.Logger
5265 onData OnDataFunc
5366
54- cmd * exec.Cmd
55- pipeOut * pipe
56- pipeIn * pipe
57- finalErr error
67+ cmd * exec.Cmd
68+ pipeOut * pipe
69+ pipeIn * pipe
70+ stderrPipe io.ReadCloser
71+ stderrDone chan struct {}
72+ tail * stderrTail
73+ finalErr error
5874
5975 terminate chan struct {}
6076 done chan struct {}
@@ -72,7 +88,7 @@ func Open(p Params, logger *slog.Logger, onData OnDataFunc) (*Camera, error) {
7288 logger = obs .Discard ()
7389 }
7490
75- c := & Camera {logger : logger , onData : onData }
91+ c := & Camera {logger : logger , onData : onData , tail : newStderrTail ( stderrTailLines ) }
7692
7793 if err := dumpComponent (); err != nil {
7894 return nil , err
@@ -99,11 +115,20 @@ func Open(p Params, logger *slog.Logger, onData OnDataFunc) (*Camera, error) {
99115 }
100116
101117 c .cmd = exec .Command (filepath .Join (dumpPath , executableName )) //nolint:gosec
102- // Discard helper output so it does not leak into the simulator's stdout
103- // (reserved for user-facing CLI output) or stderr. Helper-side errors
104- // reach us via the 'e' control byte on the read pipe.
118+ // Discard helper stdout so it never leaks into the simulator's user-facing
119+ // stdout. Stderr is captured into a ring buffer (and forwarded to the
120+ // simulator logger at DEBUG); on terminal failure we flush the tail at
121+ // WARN so the operator can see libcamera/V4L2 detail behind the 'e'
122+ // control byte (e.g. why VIDIOC_STREAMON failed).
105123 c .cmd .Stdout = io .Discard
106- c .cmd .Stderr = io .Discard
124+ stderrPipe , err := c .cmd .StderrPipe ()
125+ if err != nil {
126+ c .pipeOut .close ()
127+ c .pipeIn .close ()
128+ freeComponent ()
129+ return nil , fmt .Errorf ("rpicamera: stderr pipe: %w" , err )
130+ }
131+ c .stderrPipe = stderrPipe
107132 c .cmd .Env = env
108133 c .cmd .Dir = dumpPath
109134 // Detach the subprocess from the parent's process group so SIGINT/SIGTERM
@@ -112,12 +137,16 @@ func Open(p Params, logger *slog.Logger, onData OnDataFunc) (*Camera, error) {
112137 c .cmd .SysProcAttr = & syscall.SysProcAttr {Setpgid : true }
113138
114139 if err := c .cmd .Start (); err != nil {
140+ _ = c .stderrPipe .Close ()
115141 c .pipeOut .close ()
116142 c .pipeIn .close ()
117143 freeComponent ()
118144 return nil , err
119145 }
120146
147+ c .stderrDone = make (chan struct {})
148+ go c .drainStderr ()
149+
121150 c .terminate = make (chan struct {})
122151 c .done = make (chan struct {})
123152
@@ -159,6 +188,9 @@ func (c *Camera) ReloadParams(p Params) error {
159188func (c * Camera ) run () {
160189 defer close (c .done )
161190 c .finalErr = c .runInner ()
191+ if c .finalErr != nil && ! errors .Is (c .finalErr , errTerminated ) {
192+ c .flushStderrTail (c .finalErr )
193+ }
162194}
163195
164196func (c * Camera ) runInner () error {
@@ -178,13 +210,15 @@ func (c *Camera) runInner() error {
178210 c .pipeIn .close ()
179211 c .pipeOut .close ()
180212 <- readDone
213+ <- c .stderrDone
181214 return err
182215
183216 case err := <- readDone :
184217 _ = c .pipeOut .write ([]byte {'e' })
185218 <- cmdDone
186219 c .pipeIn .close ()
187220 c .pipeOut .close ()
221+ <- c .stderrDone
188222 return err
189223
190224 case <- c .terminate :
@@ -193,11 +227,54 @@ func (c *Camera) runInner() error {
193227 c .pipeOut .close ()
194228 c .pipeIn .close ()
195229 <- readDone
196- return fmt .Errorf ("rpicamera: terminated" )
230+ <- c .stderrDone
231+ return errTerminated
197232 }
198233 }
199234}
200235
236+ // drainStderr reads helper stderr line-by-line, mirrors each into the
237+ // simulator log at DEBUG, and keeps the most recent lines in tail so a
238+ // terminal error can attach the relevant libcamera/V4L2 detail at WARN.
239+ //
240+ // A scanner failure (bufio.ErrTooLong on a >256 KiB line, or a read error
241+ // from the pipe) is itself diagnostic context — record it into the tail
242+ // and log it at WARN so the post-mortem flush carries the reason the
243+ // stderr stream stopped early.
244+ func (c * Camera ) drainStderr () {
245+ defer close (c .stderrDone )
246+ scanner := bufio .NewScanner (c .stderrPipe )
247+ // libcamera lines occasionally exceed the default 64 KiB token cap on
248+ // verbose IPA banners. 256 KiB is overkill but cheap.
249+ scanner .Buffer (make ([]byte , 64 * 1024 ), 256 * 1024 )
250+ for scanner .Scan () {
251+ line := scanner .Text ()
252+ c .tail .push (line )
253+ c .logger .Debug ("rpicamera: helper stderr" , "line" , line )
254+ }
255+ if err := scanner .Err (); err != nil {
256+ c .tail .push (fmt .Sprintf ("<stderr scanner error: %s>" , err ))
257+ c .logger .Warn ("rpicamera: helper stderr scanner stopped" , "err" , err )
258+ }
259+ }
260+
261+ // flushStderrTail emits the captured stderr ring at WARN when the camera is
262+ // torn down with a non-nil error. Operator-visible: this is the load-bearing
263+ // diagnostic for "encoder_create()", "pipeline_handler" and similar helper
264+ // failures whose root cause libcamera prints to stderr but mtxrpicam relays
265+ // only as a short string over the 'e' control byte.
266+ func (c * Camera ) flushStderrTail (cause error ) {
267+ lines := c .tail .snapshot ()
268+ if len (lines ) == 0 {
269+ return
270+ }
271+ c .logger .Warn (
272+ "rpicamera: helper stderr tail" ,
273+ "err" , cause .Error (),
274+ "lines" , lines ,
275+ )
276+ }
277+
201278func (c * Camera ) runReader () error {
202279 // Wait for the helper's "ready" sentinel before forwarding access units.
203280 for {
0 commit comments