Skip to content

shutdown: synchronize station-device teardown#1035

Open
VA3THP wants to merge 2 commits into
foldynl:masterfrom
VA3THP:fix-station-device-shutdown
Open

shutdown: synchronize station-device teardown#1035
VA3THP wants to merge 2 commits into
foldynl:masterfrom
VA3THP:fix-station-device-shutdown

Conversation

@VA3THP
Copy link
Copy Markdown

@VA3THP VA3THP commented May 16, 2026

Summary

This fixes an intermittent crash I saw when closing QLog on Windows with rigctld
sharing enabled. The crash was in RigctldManager::stop(), called from
Rig::~Rig(), while QProcess::waitForFinished() was running.

The old shutdown path queued Rig, Rotator, and CWKeyer cleanup to their worker
threads, then slept for a fixed amount of time before the rest of QLog shut down.
That could leave device cleanup racing Qt/application teardown.

This change makes shutdown explicit:

  • disconnect station-device signals before the UI is destroyed
  • shut down CWKeyer, Rotator, then Rig on their worker threads
  • join the worker threads after app.exec() returns
  • make RigctldManager::stop() safe to call more than once
  • avoid deleting a still-running QThread if shutdown times out

Validation

  • Fresh MSVC build: PASSED
  • RigctldManagerTest: PASSED
  • QLog launch/close smoke test: PASSED
  • rigctld-sharing close test: PASSED

Notes

I tested the Rig/rigctld path locally. I do not have Rotator or CWKeyer hardware,
so those paths follow the same shutdown pattern but have not been tested with
real hardware.

Replace the close-time queued Rig/Rotator/CWKeyer shutdown plus fixed sleep with synchronous shutdown paths on each owning worker thread. MainWindow now disconnects station-device singleton signals before teardown and shuts down CWKeyer, Rotator, then Rig before the worker threads are joined.

Harden rigctld process cleanup by making RigctldManager::stop() safe to call more than once and by avoiding late cross-thread process shutdown from destructors. Also avoid deleting worker QThread objects if they fail to stop within the bounded join timeout.

This fixes an intermittent Windows close crash observed with rigctld sharing active:

  qlog.exe!RigctldManager::stop() Line 149

  qlog.exe!Rig::~Rig() Line 763

The crash occurred while QProcess::waitForFinished() was running during application teardown.

Validation: RigctldManagerTest passed, 19 passed / 0 failed / 0 skipped.
@foldynl
Copy link
Copy Markdown
Owner

foldynl commented May 18, 2026

Thanks for the PR. I only made a small change: the shutdown still runs on the worker thread, but it now uses a queued shutdown request with a timeout guard, so the application will not wait forever if a device does not respond while closing.

The overall principle of your patch remains the same. I also kept the regular CW keyer close path using deferred deletion, so normal reconnect/error handling should keep the previous behavior.

Could you please test it and confirm whether it still works correctly on your setup? Branch VA3THP_1035

@foldynl foldynl moved this to In Progress in v0.51.0 Roadmap May 18, 2026
@VA3THP
Copy link
Copy Markdown
Author

VA3THP commented May 18, 2026

Thank you very much for all your hard work on QLog.

I can see why adding a timeout here is useful, thanks for that. There is an edge case I’m worried about in regard to what happens after that timeout fires.

At that point, the main thread no longer knows whether the shutdown work is still queued, already running, or finished. Teardown keeps going anyway, so shutdown() no longer clearly means “device cleanup is done.”

If you prefer the top-level timeout version, I think the queued callback should handle the timeout case explicitly. I pushed a version close to your branch into the PR with a few changes:

  • the semaphore is shared-owned, so a late callback cannot reference a destroyed stack object;
  • the queued callback checks a shared cancellation flag before starting shutdownImpl();
  • CWKeyer keeps immediate deletion in __closeCWKey().

The cancellation flag does not make cleanup complete, and it cannot stop shutdownImpl() once it has started. It only prevents late cleanup from starting after the main thread has already given up and moved on to thread teardown. That seems safer than letting a late callback start device cleanup during teardown.

I like the QMutexLocker cleanup. I would probably keep the CWKeyer deleteLater() change separate unless it is needed for this fix. The normal reopen path calls __closeCWKey() and then immediately creates and opens the replacement key. With deferred deletion, the old key’s destructor may run after the replacement has already opened, so that path depends more on close() releasing every resource synchronously.

I defer to your judgment, thanks so much.

I built and ran it locally on Windows, all tests pass, have not experienced a crash.

@foldynl foldynl moved this from In Progress to Done - branch: testing_0.51 in v0.51.0 Roadmap May 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done - branch: testing_0.51

Development

Successfully merging this pull request may close these issues.

2 participants