Skip to content

Fix radio control self-deadlock on rigctld connection errors - #431

Merged
csete merged 1 commit into
csete:masterfrom
N6RFM:fix-radio-deadlock-425
Oct 11, 2026
Merged

csete merged 1 commit into
csete:masterfrom
N6RFM:fix-radio-deadlock-425

Conversation

@N6RFM

@N6RFM N6RFM commented Oct 10, 2026

Copy link
Copy Markdown
Contributor

Fixes #425.

When the rigctld error count reaches MAX_ERROR_COUNT, rigctl_run()
called gtk_toggle_button_set_active() from the worker thread. That
re-entered rig_engaged_cb() on the same thread, which blocked forever
in g_cond_wait() on widgetready (only rigctl_run() signals it). The
socket was never closed, so the next Engage reused the dead socket.
Calling GTK widget functions off the main thread is also undefined
behavior in GTK3.

This does the cleanup on the worker thread (engaged flag, errcnt,
socket, timer) and moves the GTK updates to the main thread via
g_idle_add(), blocking the toggled handler so rig_engaged_cb()'s
user-initiated disengage branch does not run twice.

Tested with a fake rigctld-protocol server that is killed and
restarted. Before: deadlock, the "MAX_ERROR_COUNT reached" message is
never logged, and no reconnect happens. After: clean disengage, and
the next Engage connects to the restarted server and completes the
handshake. Builds on current master (GTK 3.24.41, GLib 2.80.0); the
g_signal_handlers_block_by_func calls give the usual -Wpedantic
function-pointer cast warning.

When the rigctld connection error count reaches MAX_ERROR_COUNT,
rigctl_run() (the background worker thread) called
gtk_toggle_button_set_active() on the Engage button directly. That
call synchronously re-enters the same thread via rig_engaged_cb's
disengage branch, which does:

    g_mutex_lock(&ctrl->widgetsync);
    setconfig(ctrl);
    g_cond_wait(&ctrl->widgetready, &ctrl->widgetsync);
    g_mutex_unlock(&ctrl->widgetsync);

The only code that ever signals widgetready is rigctl_run's own
'else' branch further up in the same function -- code that cannot
run until this call returns. The result is a permanent self-deadlock:
the thread parks forever, and because rigctrl_close() (which resets
ctrl->sock to 0) lives in that same unreachable branch, it never
runs either.

This has two observable consequences, both confirmed in a headless
sandbox reproduction (Xvfb + a minimal fake rigctld-protocol TCP
server, killed and restarted to simulate a GRC flowgraph restarting):

1. Every subsequent Engage attempt reuses the dead, non-zero
   ctrl->sock instead of reopening a connection, since rigctl_run's
   own check (if (!t_ctrl->sock) rigctrl_open(t_ctrl);) sees a
   stale-but-nonzero value and skips reconnecting entirely. Verified
   directly: after simulating a flowgraph restart, the new listening
   server received zero connection attempts, while Gpredict kept
   logging errors against the old, closed socket.

2. gtk_toggle_button_set_active() and gtk_widget_set_sensitive() were
   being called directly from a non-main thread, which is undefined
   behavior in GTK3 (not thread-safe) and a plausible source of the
   intermittent full-application freezes reported alongside this,
   independent of the deadlock itself.

Fix: perform the actual cleanup (engaged flag, error count reset,
socket close, timer removal) directly on the worker thread, where it
belongs, then marshal only the GTK widget updates onto the main
thread via g_idle_add(). The toggled signal is blocked while updating
the button so rig_engaged_cb's own disengage branch -- meant for
user-initiated Engage/Disengage on the main thread -- does not run a
second time. The worker thread then breaks out of its loop and exits
cleanly, exactly as the normal (user-initiated) disengage path does.

Verified in sandbox: after this fix, MAX_ERROR_COUNT correctly logs
and disengages (previously unreachable), and a subsequent Engage
successfully opens a fresh connection to a restarted server, with a
full handshake and normal command cycling.

Fixes csete#425
@csete

csete commented Oct 11, 2026

Copy link
Copy Markdown
Owner

It's merged, thanks!

@csete
csete merged commit 21e95dc into csete:master Oct 11, 2026
1 check passed
@csete

csete commented Oct 11, 2026

Copy link
Copy Markdown
Owner

By the way, I pushed a change which eliminates the function-pointer cast warning - please let me know if it breaks it :)

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.

Radio control self-deadlock + cross-thread GTK calls on rigctld connection errors

2 participants