Follow-up fixes for fyne-ui-v2 (PR #174 review comments) - #177
Conversation
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.
|
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
I also checked the join for deadlock: Two follow-ups (not blockers)1. Impact today is nil — 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 ( Note for #175This 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 |
Summary
Follow-up fixes addressing the review comments raised on #174.
Client / modem (goroutine & socket lifecycle)
Client, created on Connect and closed on Disconnect.mc.quit, so a disconnect mid-handshake doesn't leave a goroutine alive for 120 s.Chat window
Engine link
ui_communication.c
spec_runflag + pthread_join (thread now joinable);spec_tid = 0on create failure so re-enable isn't blocked.