From 97ec48534edd815effc0a77e419781949f5f0db5 Mon Sep 17 00:00:00 2001 From: Brandon McGinty Date: Thu, 20 Aug 2026 14:33:47 -0400 Subject: [PATCH] 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 --- config/notification_unix.go | 7 +++++ config/notification_windows.go | 9 +++++++ config/user_config.go | 2 +- main.go | 48 ++++++++++++++++------------------ notification_unix.go | 9 +++++++ notification_windows.go | 9 +++++++ ui.go | 7 ++++- 7 files changed, 63 insertions(+), 28 deletions(-) create mode 100644 config/notification_unix.go create mode 100644 config/notification_windows.go create mode 100644 notification_unix.go create mode 100644 notification_windows.go diff --git a/config/notification_unix.go b/config/notification_unix.go new file mode 100644 index 0000000..58a121c --- /dev/null +++ b/config/notification_unix.go @@ -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\"" +} diff --git a/config/notification_windows.go b/config/notification_windows.go new file mode 100644 index 0000000..fe62cb3 --- /dev/null +++ b/config/notification_windows.go @@ -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 "" +} diff --git a/config/user_config.go b/config/user_config.go index 529dea2..5cdfb22 100644 --- a/config/user_config.go +++ b/config/user_config.go @@ -150,7 +150,7 @@ func (c *Config) LoadConfig() { jc.Username = &username } if c.config.NotifyCommand == nil { - ncmd := string("/usr/share/barnard/barnard-sound.sh \"%event\" \"%who\" \"%what\"") + ncmd := defaultNotifyCommand() jc.NotifyCommand = &ncmd } if c.config.NoiseSuppressionEnabled == nil { diff --git a/main.go b/main.go index 4398e51..8ddde60 100644 --- a/main.go +++ b/main.go @@ -9,7 +9,6 @@ import ( "log" "net/http" "os" - "os/exec" "strings" "time" @@ -45,31 +44,28 @@ func do_list_devices() { show_devs("Inputs:", idevs) } -func setup_notify_runner(notify_command string) chan []string { - t := make(chan []string) - var do_nothing = false - var err error - if err != nil { - } - if notify_command == "" { - do_nothing = true - } - go func(events chan []string, cmd_template string, dummy bool) { - for { - event := <-events - 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" - args := []string{"-c", t} - x := exec.Command(cmd, args...) - x.Run() - } //if we actually have a command to run - } //for - }(t, notify_command, do_nothing) - return t +const notificationQueueSize = 32 + +func setup_notify_runner(notifyCommand string) chan []string { + events := make(chan []string, notificationQueueSize) + go func() { + for event := range events { + if notifyCommand != "" { + runNotification(expandNotification(notifyCommand, event)) + } + } + }() + return events +} + +// 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 main() { diff --git a/notification_unix.go b/notification_unix.go new file mode 100644 index 0000000..946395c --- /dev/null +++ b/notification_unix.go @@ -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() +} diff --git a/notification_windows.go b/notification_windows.go new file mode 100644 index 0000000..5c969e1 --- /dev/null +++ b/notification_windows.go @@ -0,0 +1,9 @@ +//go:build windows + +package main + +import "os/exec" + +func runNotification(command string) { + _ = exec.Command("cmd.exe", "/C", command).Run() +} diff --git a/ui.go b/ui.go index 5afe6ca..0f67af3 100644 --- a/ui.go +++ b/ui.go @@ -39,7 +39,12 @@ func esc(str string) 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() {