Bound notification delivery and expansion

This commit is contained in:
Brandon McGinty (chatgpt)
2026-08-09 14:38:02 -04:00
committed by Brandon McGinty
parent 4497d41077
commit d64e5125dd
4 changed files with 43 additions and 12 deletions
+19
View File
@@ -4,6 +4,7 @@ import (
"io" "io"
"strings" "strings"
"testing" "testing"
"time"
"git.stormux.org/storm/barnard/gumble/gumble" "git.stormux.org/storm/barnard/gumble/gumble"
) )
@@ -34,6 +35,24 @@ func TestReadFIFOStopsOnEOF(t *testing.T) {
} }
} }
// Regression: sequential substitutions re-expanded placeholders embedded in
// server-provided fields, and a slow notifier blocked callback goroutines.
func TestNotificationExpansionIsSinglePassAndNotifyDoesNotBlock(t *testing.T) {
got := expandNotification("%event %what", []string{"event", "who", "%event"})
if got != "event %event" {
t.Fatalf("unexpected expansion %q", got)
}
b := &Barnard{notifyChannel: make(chan []string, 1)}
b.Notify("one", "", "")
done := make(chan struct{})
go func() { b.Notify("two", "", ""); close(done) }()
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("Notify blocked on a full queue")
}
}
func TestUserChangeNotification(t *testing.T) { func TestUserChangeNotification(t *testing.T) {
current := &gumble.Channel{ID: 1, Name: "Current"} current := &gumble.Channel{ID: 1, Name: "Current"}
other := &gumble.Channel{ID: 2, Name: "Other"} other := &gumble.Channel{ID: 2, Name: "Other"}
+1 -1
View File
@@ -136,7 +136,7 @@ Priority 1: transport, lifecycle, and correctness
lists or strand goroutines. Use mutex-protected listener snapshots and an lists or strand goroutines. Use mutex-protected listener snapshots and an
idempotent detach operation. idempotent detach operation.
17. Notification commands block callers and substitution is unsafe [x] 17. Notification commands block callers and substitution is unsafe
File: main.go File: main.go
Notify sends to an unbuffered channel. The one consumer waits for each Notify sends to an unbuffered channel. The one consumer waits for each
shell command, so slow notification programs block UI/network callbacks. shell command, so slow notification programs block UI/network callbacks.
+17 -10
View File
@@ -47,8 +47,10 @@ func do_list_devices() {
show_devs("Inputs:", idevs) show_devs("Inputs:", idevs)
} }
const notificationQueueSize = 32
func setup_notify_runner(notify_command string) chan []string { func setup_notify_runner(notify_command string) chan []string {
t := make(chan []string) t := make(chan []string, notificationQueueSize)
var do_nothing = false var do_nothing = false
var err error var err error
if err != nil { if err != nil {
@@ -57,23 +59,28 @@ func setup_notify_runner(notify_command string) chan []string {
do_nothing = true do_nothing = true
} }
go func(events chan []string, cmd_template string, dummy bool) { go func(events chan []string, cmd_template string, dummy bool) {
for { for event := range events {
event := <-events
if !dummy { if !dummy {
t := string(cmd_template)
t = strings.ReplaceAll(t, "%event", shellescape.Quote(event[0]))
t = strings.ReplaceAll(t, "%who", shellescape.Quote(event[1]))
t = strings.ReplaceAll(t, "%what", shellescape.Quote(event[2]))
cmd := "/bin/sh" cmd := "/bin/sh"
args := []string{"-c", t} args := []string{"-c", expandNotification(cmd_template, event)}
x := exec.Command(cmd, args...) x := exec.Command(cmd, args...)
x.Run() x.Run()
} //if we actually have a command to run }
} //for }
}(t, notify_command, do_nothing) }(t, notify_command, do_nothing)
return t return t
} }
// expandNotification replaces placeholders in one pass so text supplied for
// one field cannot cause another placeholder to be expanded recursively.
func expandNotification(template string, event []string) string {
return strings.NewReplacer(
"%event", shellescape.Quote(event[0]),
"%who", shellescape.Quote(event[1]),
"%what", shellescape.Quote(event[2]),
).Replace(template)
}
func setup_fifo(fn string) (chan string, error) { func setup_fifo(fn string) (chan string, error) {
t := make(chan string) t := make(chan string)
if fn == "" { if fn == "" {
+6 -1
View File
@@ -38,7 +38,12 @@ func esc(str string) string {
} }
func (b *Barnard) Notify(event string, who string, what string) { func (b *Barnard) Notify(event string, who string, what string) {
b.notifyChannel <- []string{event, who, what} // Notifications are best-effort: a slow external command must not block a
// UI or network callback. New events are dropped once the bounded queue is full.
select {
case b.notifyChannel <- []string{event, who, what}:
default:
}
} }
func (b *Barnard) SetSelectedUser(user *gumble.User) { func (b *Barnard) SetSelectedUser(user *gumble.User) {