Skip to content

feat: send messages while sync --follow is running (IPC delegation)#92

Open
najibninaba wants to merge 9 commits intosteipete:mainfrom
najibninaba:feat/ipc-send-delegation
Open

feat: send messages while sync --follow is running (IPC delegation)#92
najibninaba wants to merge 9 commits intosteipete:mainfrom
najibninaba:feat/ipc-send-delegation

Conversation

@najibninaba
Copy link
Copy Markdown

Summary

Fixes #70.

wacli send text and wacli send file now work while sync --follow is running. When the store lock is held, the send command automatically delegates to the running sync process via a Unix-domain socket (<store>/.send.sock). This is fully transparent — output is identical to a direct send.

# Terminal 1
wacli sync --follow

# Terminal 2 — just works now
wacli send text --to 6591234567 --message "hello"
wacli send file --to 6591234567 --file ./report.pdf --caption "Q3 report"

Design

The fallback chain in each send command:

  1. Try to acquire the store lock and send directly (unchanged behavior)
  2. If ErrLocked → attempt IPC delegation via .send.sock
  3. If socket unavailable (e.g. sync --once) → return the original lock error
  4. If the sync process returns an app error (e.g. not authenticated) → surface it
  5. If delegation succeeds → format output identically to a direct send

Only sync --follow hosts the IPC server. sync --once and auth do not.

Architecture

  • Lock sentinel (internal/lock): ErrLocked with errors.Is support for clean branching
  • Send refactor (internal/app/send.go): App.SendText/SendFile extracted from CLI layer
  • IPC transport (internal/app/send_ipc.go): JSON-over-UDS protocol (version 1), client + server
  • Sync hosting (internal/app/sync.go): follow-mode starts/stops the IPC server
  • CLI fallback (cmd/wacli/send.go, send_file_cmd.go): lock → IPC → original error
  • Concurrency safety: connectMu serializes Connect; IPC server serializes delegated sends; file paths normalized to absolute before delegation

Robustness

  • Server-side 1s read deadline prevents stalled clients from blocking future sends
  • Active connection tracking + force-close in Close() ensures prompt shutdown
  • Stale socket cleanup on next startup (crash recovery)
  • Socket permissions hardened to 0600
  • File send responses validated for File != nil (prevents nil-pointer on malformed peers)

Commits

Commit Description
2d8e812 ErrLocked sentinel + shared resolveStoreDir
48e0dc3 Extract App.SendText/SendFile into internal/app/send.go
056a78c IPC transport layer (client + server)
f6e7a57 Follow-mode sync hosts IPC server + connectMu serialization
7ad832a CLI send commands: lock → IPC fallback
1958319 README + spec documentation
cfa8c7c Server read deadline + active-conn tracking for robust shutdown
8c3f535 File response validation + socket permissions 0600
7a559f3 Concurrent delegated sends test

Test coverage

  • Lock contention detection (lock_test.go)
  • Send behavior: text/file classification, persistence, best-effort DB failures (send_test.go)
  • IPC round-trip, missing socket, version mismatch, timeout encoding, stale socket cleanup (send_ipc_test.go)
  • Stalled client does not block shutdown (send_ipc_test.go)
  • Malformed file response rejected with protocol error (send_ipc_test.go)
  • Socket permissions = 0600 (send_ipc_test.go)
  • Follow-mode hosts socket, delegated send succeeds, socket removed on exit (sync_test.go)
  • Once-mode does not host socket (sync_test.go)
  • 3 concurrent delegated sends all succeed (sync_test.go)
  • AST regression: connectMu lock discipline in Connect (locking_regression_test.go)

All tests pass with -race.

Breaking changes

None. Existing behavior is unchanged when the lock is free.

- Add lock.ErrLocked sentinel and ContentionError typed error
- Acquire() returns *ContentionError wrapping ErrLocked on flock contention
- Non-contention flock errors return distinct 'flock lock file' error
- Add resolveStoreDir() helper in root.go, used by newApp and doctor
- Doctor only reports LockHeld=true on actual contention (errors.Is)
- Non-contention lock probe errors in doctor are surfaced, not mislabeled
- Tests verify errors.Is(err, ErrLocked) and errors.As(*ContentionError)

Task 1 of IPC send delegation plan.
- Add SendTextParams, SendFileParams, SendFileMetadata, SendResult types
- Implement App.SendText() and App.SendFile() as canonical send entry points
- Move MIME detection, upload, proto construction, DB persistence to app layer
- Consolidate chatKindFromJID (send_file.go) with chatKind (sync.go)
- Extend fakeWA with send/upload recording and configurable responses
- CLI commands now only do flag parsing + output formatting
- Delete cmd/wacli/send_file.go (all logic moved to internal/app)
- Tests cover text send, file classification, and best-effort persistence
- Best-effort DB persistence: post-send DB errors don't fail the command

Task 2 of IPC send delegation plan.
- Add SendSocketPath(storeDir) for canonical socket location
- Add ErrSendDelegateUnavailable sentinel for transport failures
- Add SendDelegateRemoteError for server-side application errors
- Add SendDelegateProtocolError for version mismatch
- Add DelegateSendText/DelegateSendFile client functions
- Add sendDelegateServer with sequential accept loop
- JSON-over-Unix-socket, one req/resp per connection
- Stale socket cleanup on server start (only removes actual sockets)
- Socket file removed on server close and serve loop exit
- Timeout propagation via TimeoutMS derived from context deadline
- Tests cover: round trips, missing/refused socket, remote errors,
  protocol version mismatch, stale cleanup, close cleanup, timeout

Task 3 of IPC send delegation plan.
- Start send delegate server in App.Sync for SyncModeFollow only
- Server starts after event handler registration, before Connect
- Server shutdown deferred on all sync exit paths
- Socket startup failure in follow mode is fatal (prevents partial state)
- SyncModeOnce and SyncModeBootstrap do not host the socket
- Add connectMu sync.Mutex to wa.Client for connect serialization
- Connect() serialized end-to-end with connectMu.Lock/defer Unlock
- ReconnectWithBackoff inherits serialization via Connect
- Integration test: delegated send succeeds during follow-mode sync
- Integration test: once mode does not create socket
- AST structural test verifies connectMu lock discipline in Connect
- All tests pass with -race flag

Task 4 of IPC send delegation plan.
- On newApp lock contention (ErrLocked), attempt IPC delegation
- If IPC unavailable (no socket), return original lock error unchanged
- If IPC returns remote error, surface that error directly
- If IPC succeeds, format output identically to direct send
- Non-lock newApp failures return unchanged (no delegation attempt)
- Normalize --file to absolute path before delegation or direct send
- JSON/human output format is identical for direct and delegated sends

Fallback chain: ErrLocked → try socket → ErrUnavailable → lock error

Task 5 of IPC send delegation plan.
- Add 0.3.0 release notes for automatic IPC delegation
- Add 'Send while syncing' section with usage examples
- Add 'Send delegation (IPC)' section to spec with fallback chain
- Document .send.sock in storage layout
- Describe protocol versioning, serialization, and cleanup

Task 6 of IPC send delegation plan.
…t shutdown

- Add 1s sendDelegateReadTimeout so stalled clients cannot block future sends
- Track activeConn in sendDelegateServer with activeConnMu guard
- handleConn registers/deregisters active conn and sets read deadline before Decode
- Close() force-closes active connection before waiting for serve() to exit
- Add TestSendDelegateServer_CloseReturnsPromptlyWithStalledClient regression test
- Existing IPC tests and sync tests continue to pass with -race

Fixes review MF1 (head-of-line blocking) and S3 (missing stalled client test).
…ions

- delegateSend rejects send_file responses where Result.File is nil
  with SendDelegateProtocolError (prevents nil-pointer panic in CLI)
- startSendDelegateServer chmod's socket to 0600 after ListenUnix
- On chmod failure, listener is closed and socket removed before error
- Add TestDelegateSendFile_RejectsNilFileMetadata regression test
- Add TestSendDelegateServer_SocketPermissions regression test

Fixes review S1 (response shape validation) and S2 (socket permissions).
- Fire 3 concurrent DelegateSendText calls during follow-mode sync
- Verify all 3 succeed and all 3 reach the WA send call
- Validates server serialization handles queued concurrent clients
- Passes with -race flag

Fixes review S4 (missing concurrent delegation test).
@nsfilho
Copy link
Copy Markdown

nsfilho commented Mar 20, 2026

image

Thank you so much for taking the initiative to try and solve this issue! However, it seems there might still be a bug in the code. After running wacli --follow, when I try to send a message, it just hangs indefinitely.

I hope the attached image helps shed some light on it. Thanks in advance if you have any ideas about what might be going on!

Final error message:

remote send error: failed to get device list: failed to send usync query: info query timed out

@sserrano44
Copy link
Copy Markdown

Tested on macOS with a locally built wacli 0.5.0.

Build:
go build -tags sqlite_fts5 -o ./dist/wacli ./cmd/wacli

Repro before this change:

  • Terminal 1: ./dist/wacli sync --follow
  • Terminal 2: ./dist/wacli send text --to "12036340724.....@g.us" --message "Test"
  • Previous behavior: store is locked (another wacli is running?)

Result with current build:

  • Same two-terminal flow now works
  • Message sent successfully while sync --follow remained running
  • Output:
    Sent to 1203634072....@g.us (id ...)

Notes:

  • I used ./dist/wacli explicitly in both terminals to avoid mixing with any older brew-installed binary.
  • In my case this fixes the original lock-contention workflow from Store lock prevents sending while sync is running #70.
  • I did not hit the hanging/timeout issue another commenter mentioned.

@Umair444
Copy link
Copy Markdown

I have tested it on Arch and this patch is working. @steipete, please merge it into main.

@steipete steipete requested a review from dinakars777 as a code owner April 21, 2026 03:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Store lock prevents sending while sync is running

4 participants