]> git.giacomolli.uk - onechat/commitdiff
Pass caller contexts through WhatsApp callbacks
authorAnderson A. Giacomolli <redacted>
Tue, 10 Nov 2026 17:43:29 +0000 (14:43 -0300)
committerAnderson A. Giacomolli <redacted>
Tue, 10 Nov 2026 17:43:29 +0000 (14:43 -0300)
Library callbacks use the context of WhatsApp.Run, so daemon
cancellation ends name lookups and image downloads. Each image keeps its
own deadline without the shorter name lookup deadline. Tests use their
caller context.

daemon_log_test.go
whatsapp.go
whatsapp_image_test.go
whatsapp_inbox.go

index 31a327878c485c60ea752cb1dfc4ec578b730146..b3a0fa42375880c20253743f192a0808d17a088e 100644 (file)
@@ -1,7 +1,6 @@
 package main
 
 import (
 package main
 
 import (
-       "context"
        "errors"
        "io"
        "log/slog"
        "errors"
        "io"
        "log/slog"
@@ -124,7 +123,7 @@ func TestDaemonLogReturnsWriteFailure(t *testing.T) {
                mutex: &sync.Mutex{},
        }
 
                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")
 }
        require.ErrorIs(t, err, io.ErrClosedPipe)
        require.ErrorContains(t, err, "write daemon log")
 }
index f225756e1efb64643a5cfbecf95d6a9ead54f3e5..be1acc65b8e763b131f107513b3efd9c2e71af0f 100644 (file)
@@ -107,6 +107,7 @@ type WhatsAppClient interface {
 // 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{
 // 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,
                dir:         filepath.Join(dir, whatsappDirName),
                clock:       clock,
                logger:      logger,
@@ -169,6 +170,9 @@ func openWhatsApp(ctx context.Context, dir string, clock func() time.Time, logge
 // creation, close, removal, and snapshots. It is held before mutex, and stays
 // held when removal releases mutex for client callbacks.
 //
 // 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.
 // 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.
@@ -181,6 +185,7 @@ func openWhatsApp(ctx context.Context, dir string, clock func() time.Time, logge
 // log, and those of private chats hold phone numbers, because the agent needs
 // them to send.
 type WhatsApp struct {
 // 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
        dir         string
        clock       func() time.Time
        logger      *slog.Logger
@@ -224,6 +229,7 @@ type WhatsApp struct {
 func (w *WhatsApp) Run(ctx context.Context, l *TurnLoop, notify func(Event)) error {
        w.mutex.Lock()
 
 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
        w.notify = notify
        linked := w.linked
        w.retrying = linked
@@ -900,9 +906,14 @@ func (w *WhatsApp) linkedBy(client WhatsAppClient) {
 // 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 {
 // 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:
        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)
        case *events.GroupInfo:
                if event.Name != nil {
                        w.groups.set(event.JID.String(), event.Name.Name)
@@ -987,7 +998,7 @@ func (w *WhatsApp) handle(event any) bool {
        w.mutex.Unlock()
 
        if line != "" {
        w.mutex.Unlock()
 
        if line != "" {
-               w.logger.Log(context.Background(), level, line, attrs...)
+               w.logger.Log(ctx, level, line, attrs...)
        }
 
        if notice != "" {
        }
 
        if notice != "" {
index f00bc3a5d74fd9413a9d243d6153902552351d81..919d6560c4470a1e3af407c987db4f904935141a 100644 (file)
@@ -15,6 +15,7 @@ import (
        "path/filepath"
        "strings"
        "testing"
        "path/filepath"
        "strings"
        "testing"
+       "testing/synctest"
        "time"
 
        "github.com/stretchr/testify/require"
        "time"
 
        "github.com/stretchr/testify/require"
@@ -103,6 +104,36 @@ func TestWhatsAppImageDownloadCanOutlastNameLookup(t *testing.T) {
        require.Equal(t, data, saved)
 }
 
        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)
 func TestWhatsAppImageFailureKeepsTheBatchMoving(t *testing.T) {
        dir := t.TempDir()
        whatsappTestSession(t, dir, true)
index 5121976c6d00307e8e227a57cdca558a11a64a9b..06f1c558d03b930bd52f597c4bd61e49806ca9e5 100644 (file)
@@ -228,17 +228,17 @@ func (i *WhatsAppInbox) startCatchup() {
 // 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.
 // 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()
 
        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 {
        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)
                defer cancel()
 
                message.Text = i.imageText(ctx, link, event, message.Text)