feat: surface runtime errors in engine status
Record the most recent runtime error (capture, encode, packet, audio) in the engine and expose it via Status.Err. The GUI shows it in the status label (highlighted as a danger) and in the tray tooltip, so a denied screen-share or encoder failure is visible without reading logs.
This commit is contained in:
@@ -15,6 +15,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -91,6 +92,9 @@ type Status struct {
|
|||||||
Frames int64
|
Frames int64
|
||||||
Dropped int64
|
Dropped int64
|
||||||
Conns int
|
Conns int
|
||||||
|
// Err is the most recent runtime error encountered (capture, encode,
|
||||||
|
// packet or audio), or nil if the stream is healthy.
|
||||||
|
Err error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Engine owns the capture, encode and send pipeline.
|
// Engine owns the capture, encode and send pipeline.
|
||||||
@@ -106,7 +110,16 @@ type Engine struct {
|
|||||||
start time.Time
|
start time.Time
|
||||||
stop chan struct{}
|
stop chan struct{}
|
||||||
|
|
||||||
frames atomic.Int64
|
frames atomic.Int64
|
||||||
|
errMu sync.RWMutex
|
||||||
|
lastErr error
|
||||||
|
}
|
||||||
|
|
||||||
|
// setErr records the most recent runtime error. Pass nil to clear it.
|
||||||
|
func (e *Engine) setErr(err error) {
|
||||||
|
e.errMu.Lock()
|
||||||
|
e.lastErr = err
|
||||||
|
e.errMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates an engine from cfg. Capture is opened eagerly so that
|
// New creates an engine from cfg. Capture is opened eagerly so that
|
||||||
@@ -208,11 +221,15 @@ func (e *Engine) Stop() {
|
|||||||
|
|
||||||
// Status returns a snapshot of the running engine.
|
// Status returns a snapshot of the running engine.
|
||||||
func (e *Engine) Status() Status {
|
func (e *Engine) Status() Status {
|
||||||
|
e.errMu.RLock()
|
||||||
|
err := e.lastErr
|
||||||
|
e.errMu.RUnlock()
|
||||||
return Status{
|
return Status{
|
||||||
Running: e.stop != nil,
|
Running: e.stop != nil,
|
||||||
Frames: e.frames.Load(),
|
Frames: e.frames.Load(),
|
||||||
Dropped: e.sender.Dropped(),
|
Dropped: e.sender.Dropped(),
|
||||||
Conns: e.sender.NumConns(),
|
Conns: e.sender.NumConns(),
|
||||||
|
Err: err,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,6 +254,7 @@ func (e *Engine) audioLoop(src io.ReadCloser) {
|
|||||||
ts := uint64(time.Since(e.start))
|
ts := uint64(time.Since(e.start))
|
||||||
packet, perr := protocol.BuildWavePacket(ts, protocol.AudioFormatS16, sampleRate, speakers, int32(frames), buf[:n])
|
packet, perr := protocol.BuildWavePacket(ts, protocol.AudioFormatS16, sampleRate, speakers, int32(frames), buf[:n])
|
||||||
if perr != nil {
|
if perr != nil {
|
||||||
|
e.setErr(perr)
|
||||||
log.Printf("flinger: wave: %v", perr)
|
log.Printf("flinger: wave: %v", perr)
|
||||||
} else {
|
} else {
|
||||||
e.sender.Send(packet)
|
e.sender.Send(packet)
|
||||||
@@ -249,6 +267,7 @@ func (e *Engine) audioLoop(src io.ReadCloser) {
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
if !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrClosedPipe) {
|
if !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrClosedPipe) {
|
||||||
|
e.setErr(err)
|
||||||
log.Printf("flinger: audio: %v", err)
|
log.Printf("flinger: audio: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -269,6 +288,7 @@ func (e *Engine) videoLoop() {
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
if !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrClosedPipe) {
|
if !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrClosedPipe) {
|
||||||
|
e.setErr(err)
|
||||||
log.Printf("flinger: capture: %v", err)
|
log.Printf("flinger: capture: %v", err)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
@@ -285,6 +305,7 @@ func (e *Engine) videoLoop() {
|
|||||||
ts := uint64(now.Sub(e.start))
|
ts := uint64(now.Sub(e.start))
|
||||||
buf, err := e.encoder.EncodeBGRA(frame.Pix, frame.Width, frame.Height, e.cfg.Quality)
|
buf, err := e.encoder.EncodeBGRA(frame.Pix, frame.Width, frame.Height, e.cfg.Quality)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
e.setErr(err)
|
||||||
log.Printf("flinger: jpeg: %v", err)
|
log.Printf("flinger: jpeg: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -296,6 +317,7 @@ func (e *Engine) videoLoop() {
|
|||||||
buf,
|
buf,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
e.setErr(err)
|
||||||
log.Printf("flinger: packet: %v", err)
|
log.Printf("flinger: packet: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package flinger
|
package flinger
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -77,6 +78,34 @@ func TestNewRejectsBadSource(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestSetErrStatus verifies runtime errors are exposed via Status and can be
|
||||||
|
// cleared.
|
||||||
|
func TestSetErrStatus(t *testing.T) {
|
||||||
|
cfg := DefaultConfig()
|
||||||
|
cfg.Source = "pattern"
|
||||||
|
cfg.Port = 19757
|
||||||
|
|
||||||
|
eng, err := New(cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("New: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if st := eng.Status(); st.Err != nil {
|
||||||
|
t.Fatalf("expected no error initially, got %v", st.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sentinel := errors.New("test capture failure")
|
||||||
|
eng.setErr(sentinel)
|
||||||
|
if st := eng.Status(); st.Err != sentinel {
|
||||||
|
t.Errorf("expected sentinel error, got %v", st.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
eng.setErr(nil)
|
||||||
|
if st := eng.Status(); st.Err != nil {
|
||||||
|
t.Errorf("expected cleared error, got %v", st.Err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestValidate rejects out-of-range values.
|
// TestValidate rejects out-of-range values.
|
||||||
func TestValidate(t *testing.T) {
|
func TestValidate(t *testing.T) {
|
||||||
bad := []func(*Config){
|
bad := []func(*Config){
|
||||||
|
|||||||
Reference in New Issue
Block a user