Skip to content

Replace Signal receive polling with a long-lived signal-cli jsonRpc process #36

Description

@MaxGhenis

Problem

The Signal bridge spawns a fresh signal-cli JVM every few seconds, forever: the receive loop runs receive --timeout 2 --max-messages 100 per poll (internal/signallive/client.go), and every send/reaction/metadata refresh is its own invocation too.

Costs, measured during the #27 investigation:

  • JVM cold start every ~3-5s — constant CPU/battery churn (a java process perpetually respawning in Activity Monitor) plus ~21MB of native-lib extraction per spawn (now confined and cleaned per Fix libsignal temp dir leak from signal-cli subprocess churn #28/Fix races: relocate signal-cli tmp root, join bridge goroutines on Close #30, but still written to disk every poll).
  • Up to ~5s message latency — inbound Signal messages wait for the next poll.
  • Head-of-line blockingcommandMu serializes every signal-cli call behind the in-flight poll (client.go), so sends/reactions/downloads regularly wait multi-second for an idle receive to come back.
  • Polls that overrun their context deadline get killed, wasting the whole cycle.

Fix: one long-lived signal-cli jsonRpc process

signal-cli's stdio JSON-RPC mode keeps a single JVM alive: envelopes stream in as receive notifications, and sends become RPC requests (send, sendReaction, sendTyping, sendSyncRequest, listContacts, listGroups, …).

Design notes from the investigation:

  • Envelope compatibility: jsonRpc notifications wrap the same envelope JSON that receive --output json emits per line ({"jsonrpc":"2.0","method":"receive","params":{"envelope":…,"account":…}}). handleReceiveOutput/processReceiveLine can be reused by unwrapping params — the WAL, recovery queue, and quarantine logic all stay.
  • Process supervision: spawn once per bridge connect; restart with backoff on exit; SIGTERM-first cancel and the per-run tmp confinement from Fix races: relocate signal-cli tmp root, join bridge goroutines on Close #30 carry over. The goTracked/Close-join plumbing from Fix races: relocate signal-cli tmp root, join bridge goroutines on Close #30 already fits a long-lived child.
  • Sends: replace per-call runSignalCLI invocations with request/response correlation over stdin/stdout (id → pending future, timeout per request). commandMu disappears entirely.
  • Pairing stays as-is: link runs before an account exists, so it remains a separate short-lived process.
  • Fallback: keep the polling path for signal-cli builds without jsonRpc (probe --version or first-spawn failure), selected automatically — not a user flag.
  • Soak plan: run against a live paired account for a day before release; watch the receive WAL for gaps and compare message counts with the phone.

Both #27's temp-dir confinement and this change were verified against signal-cli 0.14.1 (Homebrew, gradle launcher honoring SIGNAL_CLI_OPTS).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions