Tear down lost and closed client connections safely

When a connection dropped, the listener thread deleted all of that
server's controllers immediately, before anything else was notified, so
the GUI, ResourceManager, and plugins kept dereferencing freed
controllers. Clicking Disconnect had the same problem and also ran the
teardown inline on the GUI thread, where UpdateDeviceList's
DEVICE_LIST_UPDATED is a BlockingQueuedConnection back to that thread,
so the device pages were not torn down before the controllers were
freed and a later queued onDetectionEnded dereferenced them.

Neither path frees controllers inline now. NetworkClient moves them to
an orphaned list and a dedicated teardown thread frees them: plugins
are warned first, then the same DETECTION_STARTED, UpdateDeviceList,
DETECTION_COMPLETE sequence a rescan runs tears down the device pages
that still reference the controllers, and only then are they freed, so
a controller always outlives its page. Both the connection-loss path
and the disconnect button queue to that thread, so the blocking handler
runs on the GUI thread while the controllers are still alive, and
StopClient's join stays off the GUI and listener threads.
This commit is contained in:
Ken Sanislo committed 2026-07-29 23:36:02 -07:00
1 parent 09a5b414b6
commit fb0e4f3ad9
4 files changed
+246 -28

No files matched your search

+56 -8
View File
@@ -106,6 +106,20 @@ NetworkClient::NetworkClient()
NetworkClient::~NetworkClient()
{
StopClient();
/*-----------------------------------------------------*\
| Free the controllers set aside by StopClient, plus |
| any left behind by a listener that never ran |
\*-----------------------------------------------------*/
ControllerListMutex.lock();
orphaned_controllers.insert(orphaned_controllers.end(), server_controllers.begin(), server_controllers.end());
server_controllers.clear();
ControllerListMutex.unlock();
DeleteOrphanedControllers();
}
/*---------------------------------------------------------*\
@@ -326,6 +340,36 @@ void NetworkClient::StopClient()
SignalNetworkClientUpdate(NETWORKCLIENT_UPDATE_REASON_CLIENT_STOPPED);
}
/*---------------------------------------------------------*\
| Controller teardown functions |
\*---------------------------------------------------------*/
void NetworkClient::TakeOrphanedControllers(std::vector<RGBController*>& orphaned)
{
ControllerListMutex.lock();
orphaned.insert(orphaned.end(), orphaned_controllers.begin(), orphaned_controllers.end());
orphaned_controllers.clear();
ControllerListMutex.unlock();
}
void NetworkClient::DeleteOrphanedControllers()
{
/*-----------------------------------------------------*\
| Delete outside the mutex; the controller destructor |
| waits for callbacks that are already running |
\*-----------------------------------------------------*/
std::vector<RGBController*> orphaned;
TakeOrphanedControllers(orphaned);
for(std::size_t orphaned_idx = 0; orphaned_idx < orphaned.size(); orphaned_idx++)
{
delete orphaned[orphaned_idx];
}
}
void NetworkClient::SendRequest_ControllerData(unsigned int dev_id)
{
NetPacketHeader request_hdr;
@@ -1905,23 +1949,27 @@ listen_done:
server_initialized = false;
server_connected = false;
/*-----------------------------------------------------*\
| Do not delete the controllers here; the front end |
| still holds pointers to them. Move them to |
| orphaned_controllers for the teardown thread to free |
\*-----------------------------------------------------*/
ControllerListMutex.lock();
std::vector<RGBController *> server_controllers_copy = server_controllers;
orphaned_controllers.insert(orphaned_controllers.end(), server_controllers.begin(), server_controllers.end());
server_controllers.clear();
for(size_t server_controller_idx = 0; server_controller_idx < server_controllers_copy.size(); server_controller_idx++)
{
delete server_controllers_copy[server_controller_idx];
}
ControllerListMutex.unlock();
/*-----------------------------------------------------*\
| Client info has changed, call the callbacks |
| Signal only an unplanned connection loss; on a |
| requested stop the caller runs the teardown |
\*-----------------------------------------------------*/
SignalNetworkClientUpdate(NETWORKCLIENT_UPDATE_REASON_CLIENT_DISCONNECTED);
if(client_active)
{
SignalNetworkClientUpdate(NETWORKCLIENT_UPDATE_REASON_CLIENT_DISCONNECTED);
}
}
/*---------------------------------------------------------*\