bound notification delivery and expand placeholders once

Give the notification queue a buffer and drop events when it is full.
The channel was unbuffered, so a slow or hung notification command
blocked whichever UI or network callback happened to raise the event.

Expand the command placeholders in a single pass.
Substituting %event, then %who, then %what meant text arriving in an
earlier field could contain a later placeholder and have it expanded,
letting a remote user inject their own text into the command.

Move the command runner behind a build tag and drop the POSIX default
on Windows.
The helper script it pointed at does not exist there, so the default
was a command that could only fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Brandon McGinty
2026-08-20 14:42:52 -04:00
co-authored by Claude Opus 5
parent 4f41dd4ed6
commit 97ec48534e
7 changed files with 63 additions and 28 deletions
+7
View File
@@ -0,0 +1,7 @@
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
package config
func defaultNotifyCommand() string {
return "/usr/share/barnard/barnard-sound.sh \"%event\" \"%who\" \"%what\""
}
+9
View File
@@ -0,0 +1,9 @@
//go:build windows
package config
// Windows installations do not ship the POSIX notification helper. Users can
// configure a cmd.exe-compatible command explicitly if they want notifications.
func defaultNotifyCommand() string {
return ""
}
+1 -1
View File
@@ -150,7 +150,7 @@ func (c *Config) LoadConfig() {
jc.Username = &username jc.Username = &username
} }
if c.config.NotifyCommand == nil { if c.config.NotifyCommand == nil {
ncmd := string("/usr/share/barnard/barnard-sound.sh \"%event\" \"%who\" \"%what\"") ncmd := defaultNotifyCommand()
jc.NotifyCommand = &ncmd jc.NotifyCommand = &ncmd
} }
if c.config.NoiseSuppressionEnabled == nil { if c.config.NoiseSuppressionEnabled == nil {
+20 -24
View File
@@ -9,7 +9,6 @@ import (
"log" "log"
"net/http" "net/http"
"os" "os"
"os/exec"
"strings" "strings"
"time" "time"
@@ -45,31 +44,28 @@ func do_list_devices() {
show_devs("Inputs:", idevs) show_devs("Inputs:", idevs)
} }
func setup_notify_runner(notify_command string) chan []string { const notificationQueueSize = 32
t := make(chan []string)
var do_nothing = false func setup_notify_runner(notifyCommand string) chan []string {
var err error events := make(chan []string, notificationQueueSize)
if err != nil { go func() {
for event := range events {
if notifyCommand != "" {
runNotification(expandNotification(notifyCommand, event))
} }
if notify_command == "" {
do_nothing = true
} }
go func(events chan []string, cmd_template string, dummy bool) { }()
for { return events
event := <-events }
if !dummy {
t := string(cmd_template) // expandNotification replaces placeholders in one pass so text supplied for
t = strings.ReplaceAll(t, "%event", shellescape.Quote(event[0])) // one field cannot cause another placeholder to be expanded recursively.
t = strings.ReplaceAll(t, "%who", shellescape.Quote(event[1])) func expandNotification(template string, event []string) string {
t = strings.ReplaceAll(t, "%what", shellescape.Quote(event[2])) return strings.NewReplacer(
cmd := "/bin/sh" "%event", shellescape.Quote(event[0]),
args := []string{"-c", t} "%who", shellescape.Quote(event[1]),
x := exec.Command(cmd, args...) "%what", shellescape.Quote(event[2]),
x.Run() ).Replace(template)
} //if we actually have a command to run
} //for
}(t, notify_command, do_nothing)
return t
} }
func main() { func main() {
+9
View File
@@ -0,0 +1,9 @@
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris
package main
import "os/exec"
func runNotification(command string) {
_ = exec.Command("/bin/sh", "-c", command).Run()
}
+9
View File
@@ -0,0 +1,9 @@
//go:build windows
package main
import "os/exec"
func runNotification(command string) {
_ = exec.Command("cmd.exe", "/C", command).Run()
}
+6 -1
View File
@@ -39,7 +39,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) Beep() { func (b *Barnard) Beep() {