Skip to content

Follow-up fixes for fyne-ui-v2 (PR #174 review comments) - #177

Merged
rafael2k merged 10 commits into
mercuryv2from
follow-up-fyne-ui
Aug 13, 2026
Merged

Follow-up fixes for fyne-ui-v2 (PR #174 review comments)#177
rafael2k merged 10 commits into
mercuryv2from
follow-up-fyne-ui

Conversation

@pedromessetti

Copy link
Copy Markdown
Contributor

Summary

Follow-up fixes addressing the review comments raised on #174.

Client / modem (goroutine & socket lifecycle)

  • client: guard forwarder sends — every forward now selects on the done channel, so a goroutine parked in a blocked send always observes disconnect (last sliver of the goroutine leak).
  • client: reap the five event goroutines — add a done channel to Client, created on Connect and closed on Disconnect.
  • modem: stop sending on LogCh while holding mc.mu — Connect/SendCommand now flush logs after unlocking.
  • modem: make ConnectARQ cancellable — select on mc.quit, so a disconnect mid-handshake doesn't leave a goroutine alive for 120 s.
  • modem: snapshot control conn before nilling — fixes dead alias guard and double-close in Connect/Disconnect.
  • modem: close sockets on partial Connect failure — deferred cleanup closes any conns opened before the failing dial.

Chat window

  • guard onConnect against double-tap — synchronous button disable + disconnect existing client before reconnecting.
  • singleton window — reuse the open chat window instead of evicting the incumbent control client.
  • ring-buffer the log — cap at 200 lines, no re-splitting of the widget text.
  • validate ports — surface strconv.Atoi errors instead of silently defaulting.
  • read real engine ports — new CGo bridge (mercury_ui_get_tcp_ports) reads arq_tcp_base_port / broadcast_tcp_port from config so -p/-b overrides are respected.
  • capture cw.mc locally in onARQDisconnect — consistency.

Engine link

  • retry device-list refresh — replace the one-shot 100 ms AfterFunc with ctx-cancellable retries (200 ms × 5) so slow audioio/hamlib restarts aren't reported as stale.

ui_communication.c

  • tx_gain_db under cfg_mutex — move the write inside the lock.
  • stop spectrum publisher on disable — atomic spec_run flag + pthread_join (thread now joinable); spec_tid = 0 on create failure so re-enable isn't blocked.
  • drop pthread_mutex_destroy — avoids racing a live ui_comm_set_waterfall at shutdown.

The five event goroutines only observed done while parked in the
receive select; every forward was an unguarded blocking send.  During a
sustained ARQ RX, handleIncomingARQData fills the 256-entry buffer
while forwardLog is briefly behind; if the operator disconnects right
then, the only drainer exits and a goroutine parked in the send never
re-reaches the select — leaking it.

Add a generic sendOrStop helper: select on the target channel and done,
returning false when done wins.  Use it for every LogCh/ARQChatCh/
BroadcastChatCh/StatusCh forward, including updateRemoteCall (now
threads done and returns a bool).
setTCP(true) disables the connect button inside fyne.Do, i.e. on a
later main-loop iteration, while Button.Tapped fires synchronously.
Two taps in one poll batch both ran onConnect, opening a second client
with three more sockets and evicting the first control client, leaking
client 1 (goroutines, readers, TCP conns).

Disable the connect button synchronously at the top of onConnect, and
Disconnect() any existing cw.mc (and close its done channel) before
opening a new one.  Re-enable the button if the new connect fails.
Connect() and SendCommand() sent to mc.LogCh while holding mc.mu.  A
full LogCh buffer (capacity 100) would block the send and hold the
modem mutex, stalling every reader's startup lock, IsConnected(),
SendARQData() and Disconnect() — a subsystem-wide stall driven from
the UI thread.

Connect() now collects its log lines into a local slice and flushes
them in a deferred closure after mc.mu.Unlock() (deferred before the
cleanup so it runs last).  SendCommand() captures the conn under the
lock, then logs and writes outside it.
logMsg re-split the widget's own text on every append (strings.Split
of ~1000 lines + Join + full SetText/Refresh on the main goroutine),
and fyne.Do queues onto an unbounded funcQueue.  During a multi-kB
transfer the chunk rate can exceed the rebuild rate, so the queue grows
and the UI falls behind for the whole transfer.

Keep a []string ring (newest first) in chatWindow, cap it at 200 lines,
and rebuild the Entry text with a single Join of the bounded slice — no
more re-splitting.
ConnectARQ waited only on respCh and a 120 s timeout, never mc.quit.
'Connect ARQ' to a silent peer followed by 'Disconnect modem' left the
goroutine alive for up to two minutes, after which it called
cw.setARQ(false) — re-enabling the button on a downed modem — and
cw.logMsg against a possibly-closed window.

Capture quit := mc.quit under the lock in ConnectARQ and select on it,
returning 'disconnected' immediately.  In onARQConnect, only touch the
button state if cw.mc is still the client that initiated the connect.
set_audio_config / set_radio_config used a one-shot 100 ms
time.AfterFunc to trigger a device-list re-read.  If audioio (or
hamlib) restart exceeded 100 ms, the refresh read the pre-change list
and the dialog showed a stale selection with no retry; the bare timer
also fired after Close().

Move the retry timing into the Start goroutine (which already watches
ctx): a refresh now re-reads immediately, then retries every 200 ms up
to 5 times, returning early if ctx is cancelled.  Send() now just
signals the refresh channel, so nothing outlives the link.
The alias guard mc.ARQDataConn != mc.ARQControlConn compared against a
pointer that had already been nil'd three lines above, so it was always
true.  When ARQDataConn aliases ARQControlConn (ARQDataAddr == '' branch)
and a later dial fails, the same *net.TCPConn was closed twice; likewise
Disconnect() double-closed and logged a bogus 'Disconnected from ARQ
Data.' line.

Snapshot the control conn into a local before nilling it, and compare
the data conn against that snapshot in both Connect()'s failure cleanup
and Disconnect().
onConnect discarded both strconv.Atoi errors, so a typo'd port became
0 and client.New silently substituted 8300/8100.  Those defaults were
also hardcoded, so they were wrong when mercury runs with -p/-b.

- Validate both port parses and show an error dialog (and re-enable
  the connect button) instead of silently defaulting.
- Add ui_comm_get_tcp_ports / mercury_ui_get_tcp_ports (CGo) that read
  arq_tcp_base_port / broadcast_tcp_port from the engine config under
  the cfg mutex.
- engineLink.TCPPorts() (plus a stub returning the defaults) surfaces
  them to Go; the Launch Mercury Client button reads them and passes
  them to the chat window, which pre-fills the port entries.
Match the single-read pattern used everywhere else instead of the
if cw.mc != nil { cw.mc... } two-read form.
… destroy

- tx_gain_db was written outside the cfg_mutex that was added two lines
  below; move it inside so the write is serialized with cfg_write.
- Disabling the waterfall never stopped the spectrum publisher: it kept
  waking at 20 Hz forever.  Add an atomic spec_run flag to ui_ctx that
  the publisher loop checks; on disable set it false and pthread_join
  (the thread is now joinable, not detached); on enable restart it.
  Set spec_tid = 0 on pthread_create failure so a failed start does not
  block re-enable.
- Drop pthread_mutex_destroy in ui_comm_shutdown: a live
  ui_comm_set_waterfall could have already read g_ui_ctx and lock the
  mutex after it is destroyed.  The process is exiting anyway.
@pedromessetti
pedromessetti requested a review from rafael2k August 13, 2026 02:20
@rafael2k

Copy link
Copy Markdown
Contributor

Reviewed all 10 commits. This is merge-ready — go ahead and merge it, @pedromessetti. Every item from the #174 follow-up list is addressed, and the two most subtle ones (the deferred-flush ordering and the alias snapshot) are done correctly.

Verified

Item Verdict
sendOrStop on all five forwarders Correct — and updateRemoteCall threads done through and returns bool rather than swallowing it
Connect() deferred log flush Correct LIFO: the unlock+flush defer is registered first so it runs last, and the conn-cleanup defer still runs under mc.mu. The comments say exactly this
SendCommand log outside the lock Correct. Conn can be closed between unlock and Write, which just returns an error — benign
Control-conn snapshot Fixes the real one: the guard compared against a pointer nil'd three lines above, so it was always true and the aliased conn was double-closed. Both Connect() cleanup and Disconnect() fixed
ConnectARQ cancellable quit := mc.quit captured under the lock — right, since a later Connect() reassigns it
onConnect double-tap Synchronous Disable() at the top, re-enabled on all three error paths
cw.done vs c.done Distinct channels; every close is paired with a nil assignment on the Fyne main goroutine. No double-close
spec_tid = 0 on create failure Correct — a failed start no longer wedges re-enable
Not destroying cfg_mutex Right call, and the comment explains why

I also checked the join for deadlock: spectrum_publisher_thread never takes cfg_mutex, so joining under it is safe. Builds clean here — make fyne-ui, mercury-client, and go vet on both the plain and mercury_embedded tags.

Two follow-ups (not blockers)

1. ui_comm_shutdown doesn't stop or join the thread it just made joinable (ui_communication.c:739). You removed the pthread_detach and join on the disable path, but shutdown still only does g_ui_ctx = NULL + ws_shutdown. Nothing sets spec_run = false there, so the thread is joinable and never joined.

Impact today is nil — g_ui_ctx is a static ui_ctx_t that outlives everything, and ws_broadcast_binary guards on ctx->running, which ws_shutdown clears first, so the post-teardown broadcasts are no-ops. But it's free to close, and this codebase has already paid for teardown ordering once (396568a, rings freed under live modem threads):

atomic_store_explicit(&ctx->spec_run, false, memory_order_relaxed);
if (ctx->spec_tid != 0) { pthread_join(ctx->spec_tid, NULL); ctx->spec_tid = 0; }
ws_shutdown(&ctx->ws);

2. The log flush is the one unguarded blocking send left (modem.go:122, and :199 in SendCommand). Moving it out from under mc.mu fixed the part that mattered — a full LogCh no longer stalls every reader, IsConnected, SendARQData and Disconnect. But the send itself can still park forever if nothing drains the channel, which is the same class sendOrStop exists for on the client side. mc.quit is already captured under the lock in ConnectARQ, so the same shape works here.

Note for #175

This changes the switch my TX-waterfall PR sits on, in a good way. With #177 in, turning the waterfall off stops the RX FFT, the TX FFT (I rebased #175 to gate on your modem_set_spectrum_enabled and dropped my separate setter), and the publisher thread — the whole chain, one control. I'll re-run #175's gate once this lands.

@rafael2k
rafael2k merged commit 904ee4d into mercuryv2 Aug 13, 2026
8 checks passed
@rafael2k
rafael2k deleted the follow-up-fyne-ui branch August 13, 2026 08:19
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.

2 participants