Take only acknowledgements from a desk click, and make each answer a one-time ticket
mesh/merge-gate pass: builds dunst, messenger → g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery-group group fix/notifications-after-review ready: every member ready, and composed together they pass
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: builds dunst, messenger → g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery-group group fix/notifications-after-review ready: every member ready, and composed together they pass
mesh/delivery superseded: a newer head of the same pull request
Review found that any process of the operator's account can choose a dunst action, so a click could release or stop a delivery in the operator's name; that tokens lived for ever; that a warning's answers died in ten seconds; and that a restarted notifier left dead buttons (hq ADR 0258).
This commit is contained in:
@@ -20,8 +20,10 @@ waits in this bundle for the operator's choice (dunst offers the actions on a mi
|
||||
context menu). The token chosen is said as this module's event `action-chosen`; it becomes the
|
||||
node-notifier seat's event once seats publish events of their own (novox/hq to-be 46 phase 3). Nothing
|
||||
is said for a notification closed, expired or replaced without an answer, and a wait ends after seven
|
||||
days. A restart of this bundle ends the waits it held: the notification stays shown, and its answers do
|
||||
nothing then (novox/hq ADR 0253).
|
||||
days. A notification that offers answers stays until dismissed (`expire_ms` 0). A restart of this bundle
|
||||
ends the waits it held; it says `started` {machine} once it serves, and the operator channel shows the
|
||||
open answers there again (novox/hq ADR 0258). `notify-send` prints the id before it waits, also into a
|
||||
pipe (checked on the laptop on 2026-10-08: the id at once, and "Wait timeout expired" when it expired).
|
||||
|
||||
## Tools
|
||||
|
||||
|
||||
@@ -39,10 +39,27 @@ const (
|
||||
MostChoices = 4
|
||||
// AnswerWait is how long an answer is waited for.
|
||||
AnswerWait = 7 * 24 * time.Hour
|
||||
// ChosenEvent is the event an answer is said as.
|
||||
ChosenEvent = "action-chosen"
|
||||
// ChosenEvent is the event an answer is said as; StartedEvent says this bundle started, so the answers
|
||||
// an earlier one waited for are lost and the operator channel shows them again (novox/hq ADR 0258).
|
||||
ChosenEvent = "action-chosen"
|
||||
StartedEvent = "started"
|
||||
)
|
||||
|
||||
// sayStarted says, once this bundle serves, that it started on this machine; retried while the runtime
|
||||
// cannot take it yet, and said in the log when it never could.
|
||||
func sayStarted() {
|
||||
body := map[string]any{"machine": os.Getenv("MESH_NODE")}
|
||||
var err error
|
||||
for wait := time.Second; wait <= time.Minute; wait *= 2 {
|
||||
time.Sleep(wait)
|
||||
if err = emit(StartedEvent, body); err == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "[dunst] could not say it started (answers waited for before are lost until the "+
|
||||
"operator channel shows them again): %v\n", err)
|
||||
}
|
||||
|
||||
var tokenShape = regexp.MustCompile(`^[A-Za-z0-9_-]{1,64}$`)
|
||||
|
||||
// emit says an event as this module; a seam a test replaces.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -43,7 +44,7 @@ func TestANotificationsChosenAnswerIsSaidAsAnEvent(t *testing.T) {
|
||||
t.Fatalf("%+v, %v, after %s: the id is answered before the answer", got, err, time.Since(start))
|
||||
}
|
||||
asked, _ := os.ReadFile(filepath.Join(bin, "log"))
|
||||
if !strings.Contains(string(asked), "[--wait][--action=a1b2=Release][--action=c3d4=Stop][--]") {
|
||||
if !strings.Contains(string(asked), "[--expire-time=0][--wait][--action=a1b2=Release][--action=c3d4=Stop][--]") {
|
||||
t.Fatalf("notify-send was asked %s", asked)
|
||||
}
|
||||
deadline := time.Now().Add(3 * time.Second)
|
||||
@@ -90,3 +91,20 @@ func TestActionsAreRefusedForWhatTheyCannotBe(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// **A bundle that started says so**, so the operator channel shows again the answers lost with the one
|
||||
// before.
|
||||
func TestABundleThatStartedSaysSo(t *testing.T) {
|
||||
t.Setenv("MESH_NODE", "g14")
|
||||
var said []string
|
||||
before := emit
|
||||
emit = func(event string, body any) error {
|
||||
said = append(said, fmt.Sprintf("%s %v", event, body))
|
||||
return nil
|
||||
}
|
||||
t.Cleanup(func() { emit = before })
|
||||
sayStarted()
|
||||
if len(said) != 1 || said[0] != "started map[machine:g14]" {
|
||||
t.Fatalf("%v", said)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,11 @@ func Send(n Notification) (SendResult, error) {
|
||||
if n.Icon != "" {
|
||||
args = append(args, "--icon="+n.Icon)
|
||||
}
|
||||
if len(n.Actions) > 0 && n.ExpireMS < 0 {
|
||||
// A notification that offers answers stays until it is dismissed: a warning's answers must not die
|
||||
// with dunst's ten seconds (novox/hq ADR 0258).
|
||||
n.ExpireMS = 0
|
||||
}
|
||||
if n.ExpireMS >= 0 {
|
||||
args = append(args, "--expire-time="+strconv.Itoa(n.ExpireMS))
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
go sayStarted()
|
||||
if err := stdio.Serve("", tools()); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
]
|
||||
},
|
||||
"emits": [
|
||||
"action-chosen"
|
||||
"action-chosen",
|
||||
"started"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user