package main
import (
- "context"
"errors"
"io"
"log/slog"
mutex: &sync.Mutex{},
}
- err := handler.Handle(context.Background(), slog.NewRecord(time.Now(), slog.LevelInfo, "ready", 0))
+ err := handler.Handle(t.Context(), slog.NewRecord(time.Now(), slog.LevelInfo, "ready", 0))
require.ErrorIs(t, err, io.ErrClosedPipe)
require.ErrorContains(t, err, "write daemon log")
}
// the client of a device with its event handler.
func openWhatsApp(ctx context.Context, dir string, clock func() time.Time, logger *slog.Logger, newClient func(device *store.Device, handle func(event any) bool) WhatsAppClient) (*WhatsApp, error) {
w := &WhatsApp{
+ ctx: ctx,
dir: filepath.Join(dir, whatsappDirName),
clock: clock,
logger: logger,
// creation, close, removal, and snapshots. It is held before mutex, and stays
// held when removal releases mutex for client callbacks.
//
+// ctx is the caller context until Run sets its context for library callbacks.
+// The mutex protects it, so callbacks follow daemon cancellation.
+//
// The inbox puts messages into batches and logs them, and groups holds the
// names of the groups. Both get a WhatsAppLink from link for each call, so they
// use the current device and client.
// log, and those of private chats hold phone numbers, because the agent needs
// them to send.
type WhatsApp struct {
+ ctx context.Context
dir string
clock func() time.Time
logger *slog.Logger
func (w *WhatsApp) Run(ctx context.Context, l *TurnLoop, notify func(Event)) error {
w.mutex.Lock()
+ w.ctx = ctx
w.notify = notify
linked := w.linked
w.retrying = linked
// connected again with the length of the outage. The retries of the library and
// Run between them log nothing.
func (w *WhatsApp) handle(event any) bool {
+ w.mutex.Lock()
+
+ ctx := w.ctx
+ w.mutex.Unlock()
+
switch event := event.(type) {
case *events.Message:
- return w.inbox.receive(w.link(), event)
+ return w.inbox.receive(ctx, w.link(), event)
case *events.GroupInfo:
if event.Name != nil {
w.groups.set(event.JID.String(), event.Name.Name)
w.mutex.Unlock()
if line != "" {
- w.logger.Log(context.Background(), level, line, attrs...)
+ w.logger.Log(ctx, level, line, attrs...)
}
if notice != "" {
"path/filepath"
"strings"
"testing"
+ "testing/synctest"
"time"
"github.com/stretchr/testify/require"
require.Equal(t, data, saved)
}
+func TestWhatsAppRunCancellationEndsAnImageDownload(t *testing.T) {
+ synctest.Test(t, func(t *testing.T) {
+ test := whatsappRetryTest(t)
+ test.fake.downloadWait = time.Hour
+ test.start(t)
+ synctest.Wait()
+
+ acknowledged := make(chan bool, 1)
+ go func() {
+ acknowledged <- test.fake.Emit(whatsappImageTestEvent(whatsappTestAna, "Keep this caption."))
+ }()
+
+ synctest.Wait()
+
+ select {
+ case <-acknowledged:
+ t.Fatal("download ended before daemon cancellation")
+ default:
+ }
+
+ test.cancel()
+ require.True(t, <-acknowledged)
+ require.NoError(t, <-test.done)
+ require.Empty(t, test.fake.Downloads())
+ require.Contains(t, test.logs.String(), "context canceled")
+ require.NoError(t, test.whatsapp.state.CloseAll())
+ require.Equal(t, "(image not downloaded)\nKeep this caption.", test.whatsapp.state.Closed()[0].Messages[0].Text)
+ })
+}
+
func TestWhatsAppImageFailureKeepsTheBatchMoving(t *testing.T) {
dir := t.TempDir()
whatsappTestSession(t, dir, true)
// message, so that the library does not acknowledge it, and Run stops the
// daemon. An image that cannot be downloaded or stored gets a text marker. A
// message that the batches do not take is handled with nothing to do.
-func (i *WhatsAppInbox) receive(link WhatsAppLink, event *events.Message) bool {
- ctx, cancel := context.WithTimeout(context.Background(), i.lookupTime)
+func (i *WhatsAppInbox) receive(ctx context.Context, link WhatsAppLink, event *events.Message) bool {
+ lookupCtx, cancel := context.WithTimeout(ctx, i.lookupTime)
defer cancel()
- chat, message, found := whatsappMessage(ctx, link.device, link.own, event)
+ chat, message, found := whatsappMessage(lookupCtx, link.device, link.own, event)
if !found {
return true
}
if event.Message.GetImageMessage() != nil {
- ctx, cancel := context.WithTimeout(context.Background(), i.imageTime)
+ ctx, cancel := context.WithTimeout(ctx, i.imageTime)
defer cancel()
message.Text = i.imageText(ctx, link, event, message.Text)