Skip to content

Commit b27a2d9

Browse files
jrdclaude
andcommitted
JSON-RPC: fix connect/disconnect crash, consolidate connection notifications
Three related changes to the client connection RPC introduced by this PR: 1. Crash fix. CClient::Stop() ran the event loop (QCoreApplication::processEvents) for its 100 ms settle. Called from the connect/disconnect handlers, that re-entered the JSON-RPC readyRead handler while it was still on the stack and freed a socket that was then written to: a use-after-free (stack overflow under overlapping requests). Keep the settle but do it with QThread::msleep, so no events are pumped and no re-entrancy is possible. Verified with AddressSanitizer on Linux and macOS: overlapping connect/disconnect plus 8-thread churn crash before this change and are clean after; a plain deferral and a re-entrancy guard were each insufficient. 2. Consolidate notifications. Fold jamulusclient/connecting and jamulusclient/connectingFailed into jamulusclient/connectionStateChanged, which now carries serverName and, on a failed attempt, error (serverName is omitted there, since an invalid address fails before the name is set). One notification follows the whole lifecycle; connected/disconnected are unchanged. 3. Rename jamulusclient/connect to jamulusclient/requestConnection: it returns "ok" as soon as the attempt is initiated, not when it succeeds, so the name reflects a request. Reads symmetrically with getConnectionState. docs/JSON-RPC.md regenerated. CHANGELOG: fix a client crash when connect/disconnect are driven over JSON-RPC Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PEyPQdxy6h7vQtUDCh2KY2
1 parent a474d52 commit b27a2d9

3 files changed

Lines changed: 53 additions & 78 deletions

File tree

docs/JSON-RPC.md

Lines changed: 21 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -129,24 +129,6 @@ Results:
129129
| result.version | string | The Jamulus version. |
130130

131131

132-
### jamulusclient/connect
133-
134-
Connects the client to a server. Any current connection is terminated first. The connection is established asynchronously: subscribe to the jamulusclient/connecting, jamulusclient/connected, jamulusclient/connectingFailed and jamulusclient/connectionStateChanged notifications to follow its progress.
135-
136-
Parameters:
137-
138-
| Name | Type | Description |
139-
| --- | --- | --- |
140-
| params.address | string | Socket address of the server (host:port). |
141-
| params.serverName | string | Optional human readable server name used for display purposes. Defaults to the address. |
142-
143-
Results:
144-
145-
| Name | Type | Description |
146-
| --- | --- | --- |
147-
| result | string | "ok" once the connection attempt has been initiated. |
148-
149-
150132
### jamulusclient/disconnect
151133

152134
Disconnects the client from the current server. Does nothing if the client is not connected.
@@ -309,6 +291,24 @@ Results:
309291
| result | string | "ok" or "error" if bad arguments. |
310292

311293

294+
### jamulusclient/requestConnection
295+
296+
Connects the client to a server. Any current connection is terminated first. The connection is established asynchronously: subscribe to the jamulusclient/connected and jamulusclient/connectionStateChanged notifications to follow its progress (a failed attempt arrives as connectionStateChanged with state "disconnected" and an error field).
297+
298+
Parameters:
299+
300+
| Name | Type | Description |
301+
| --- | --- | --- |
302+
| params.address | string | Socket address of the server (host:port). |
303+
| params.serverName | string | Optional human readable server name used for display purposes; if given it must be a string. Defaults to the address. |
304+
305+
Results:
306+
307+
| Name | Type | Description |
308+
| --- | --- | --- |
309+
| result | string | "ok" once the connection attempt has been initiated. |
310+
311+
312312
### jamulusclient/sendChatText
313313

314314
Sends a chat text message.
@@ -709,37 +709,17 @@ Parameters:
709709
| params.id | number | The channel ID assigned to the client. |
710710

711711

712-
### jamulusclient/connecting
713-
714-
Emitted when a connection to a server has been requested but is not yet established.
715-
716-
Parameters:
717-
718-
| Name | Type | Description |
719-
| --- | --- | --- |
720-
| params.serverName | string | The human readable server name (or the address if no name is known). |
721-
722-
723-
### jamulusclient/connectingFailed
724-
725-
Emitted when a connection attempt failed before it could be requested from the server.
726-
727-
Parameters:
728-
729-
| Name | Type | Description |
730-
| --- | --- | --- |
731-
| params.error | string | The error message. |
732-
733-
734712
### jamulusclient/connectionStateChanged
735713

736-
Emitted whenever the connection state changes.
714+
Emitted whenever the connection state changes. On a failed connection attempt it is emitted with state "disconnected" and an additional error field.
737715

738716
Parameters:
739717

740718
| Name | Type | Description |
741719
| --- | --- | --- |
742720
| params.state | string | The new connection state (disconnected, connecting, or connected). |
721+
| params.serverName | string | The human readable server name (empty/absent when disconnected). |
722+
| params.error | string | Only present on a failed connection attempt (with state "disconnected"); serverName is omitted in that case. |
743723

744724

745725
### jamulusclient/disconnected

src/client.cpp

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
\******************************************************************************/
4646

4747
#include "client.h"
48+
#include <QThread>
4849
#include "settings.h"
4950
#include "util.h"
5051

@@ -1108,19 +1109,12 @@ void CClient::Stop()
11081109
qWarning() << "Could not reinitialise the sound device while disconnecting:" << generr.GetErrorText();
11091110
}
11101111

1111-
// wait for approx. 100 ms to make sure no audio packet is still in the
1112-
// network queue causing the channel to be reconnected right after having
1113-
// received the disconnect message (seems not to gain much, disconnect is
1114-
// still not working reliably)
1115-
QTime DieTime = QTime::currentTime().addMSecs ( 100 );
1116-
while ( QTime::currentTime() < DieTime )
1117-
{
1118-
// exclude user input events because if we use AllEvents, it happens
1119-
// that if the user initiates a connection and disconnection quickly
1120-
// (e.g. quickly pressing enter five times), the software can get into
1121-
// an unknown state
1122-
QCoreApplication::processEvents ( QEventLoop::ExcludeUserInputEvents, 100 );
1123-
}
1112+
// Wait ~100 ms so no audio packet is still in the network queue causing the
1113+
// channel to be reconnected right after the disconnect message. We must NOT
1114+
// run the event loop to do this: pumping events here re-entered the JSON-RPC
1115+
// read handler and freed a socket still being written to (use-after-free). A
1116+
// plain sleep keeps the settle without re-entrancy.
1117+
QThread::msleep ( 100 );
11241118

11251119
// Send disconnect message to server (Since we disable our protocol
11261120
// receive mechanism with the next command, we do not evaluate any

src/clientrpc.cpp

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -183,33 +183,28 @@ CClientRpc::CClientRpc ( CClient* pClient, CClientSettings* pSettings, CRpcServe
183183
/// @param {object} params - No parameters (empty object).
184184
connect ( pClient, &CClient::Disconnected, [=]() { pRpcServer->BroadcastNotification ( "jamulusclient/disconnected", QJsonObject{} ); } );
185185

186-
/// @rpc_notification jamulusclient/connecting
187-
/// @brief Emitted when a connection to a server has been requested but is not yet established.
188-
/// @param {string} params.serverName - The human readable server name (or the address if no name is known).
189-
connect ( pClient, &CClient::Connecting, [=] ( QString strServerName ) {
190-
pRpcServer->BroadcastNotification ( "jamulusclient/connecting",
191-
QJsonObject{
192-
{ "serverName", strServerName },
193-
} );
194-
} );
195-
196-
/// @rpc_notification jamulusclient/connectingFailed
197-
/// @brief Emitted when a connection attempt failed before it could be requested from the server.
198-
/// @param {string} params.error - The error message.
186+
// A failed attempt surfaces through connectionStateChanged with the error attached.
187+
// serverName is omitted here: on the invalid-address path the attempt throws before the
188+
// name is set, so it is not reliably known.
199189
connect ( pClient, &CClient::ConnectingFailed, [=] ( QString strError ) {
200-
pRpcServer->BroadcastNotification ( "jamulusclient/connectingFailed",
190+
pRpcServer->BroadcastNotification ( "jamulusclient/connectionStateChanged",
201191
QJsonObject{
192+
{ "state", ConnectionStateToString ( CS_DISCONNECTED ) },
202193
{ "error", strError },
203194
} );
204195
} );
205196

206197
/// @rpc_notification jamulusclient/connectionStateChanged
207-
/// @brief Emitted whenever the connection state changes.
198+
/// @brief Emitted whenever the connection state changes. On a failed connection attempt it is
199+
/// emitted with state "disconnected" and an additional error field.
208200
/// @param {string} params.state - The new connection state (disconnected, connecting, or connected).
201+
/// @param {string} params.serverName - The human readable server name (empty/absent when disconnected).
202+
/// @param {string} params.error - Only present on a failed connection attempt (with state "disconnected"); serverName is omitted in that case.
209203
connect ( pClient, &CClient::ConnectionStateChanged, [=] ( EConnectionState eState ) {
210204
pRpcServer->BroadcastNotification ( "jamulusclient/connectionStateChanged",
211205
QJsonObject{
212206
{ "state", ConnectionStateToString ( eState ) },
207+
{ "serverName", eState == CS_DISCONNECTED ? QString() : pClient->GetConnectedServerName() },
213208
} );
214209
} );
215210

@@ -257,25 +252,31 @@ CClientRpc::CClientRpc ( CClient* pClient, CClientSettings* pSettings, CRpcServe
257252
Q_UNUSED ( params );
258253
} );
259254

260-
/// @rpc_method jamulusclient/connect
255+
/// @rpc_method jamulusclient/requestConnection
261256
/// @brief Connects the client to a server. Any current connection is terminated first.
262-
/// The connection is established asynchronously: subscribe to the jamulusclient/connecting,
263-
/// jamulusclient/connected, jamulusclient/connectingFailed and jamulusclient/connectionStateChanged
264-
/// notifications to follow its progress.
257+
/// The connection is established asynchronously: subscribe to the jamulusclient/connected
258+
/// and jamulusclient/connectionStateChanged notifications to follow its progress (a failed
259+
/// attempt arrives as connectionStateChanged with state "disconnected" and an error field).
265260
/// @param {string} params.address - Socket address of the server (host:port).
266-
/// @param {string} params.serverName - Optional human readable server name used for display purposes. Defaults to the address.
261+
/// @param {string} params.serverName - Optional human readable server name used for display purposes; if given it must be a string. Defaults to the address.
267262
/// @result {string} result - "ok" once the connection attempt has been initiated.
268-
pRpcServer->HandleMethod ( "jamulusclient/connect", [=] ( const QJsonObject& params, QJsonObject& response ) {
263+
pRpcServer->HandleMethod ( "jamulusclient/requestConnection", [=] ( const QJsonObject& params, QJsonObject& response ) {
269264
auto jsonAddress = params["address"];
270265
if ( !jsonAddress.isString() )
271266
{
272267
response["error"] = CRpcServer::CreateJsonRpcError ( CRpcServer::iErrInvalidParams, "Invalid params: address is not a string" );
273268
return;
274269
}
275270

276-
auto jsonServerName = params["serverName"];
277-
const QString strAddress = NetworkUtil::FixAddress ( jsonAddress.toString() );
278-
const QString strServerName = jsonServerName.isString() ? jsonServerName.toString() : strAddress;
271+
auto jsonServerName = params["serverName"];
272+
if ( !jsonServerName.isUndefined() && !jsonServerName.isNull() && !jsonServerName.isString() )
273+
{
274+
response["error"] = CRpcServer::CreateJsonRpcError ( CRpcServer::iErrInvalidParams, "Invalid params: serverName is not a string" );
275+
return;
276+
}
277+
278+
const QString strAddress = NetworkUtil::FixAddress ( jsonAddress.toString() );
279+
const QString strServerName = jsonServerName.isString() ? jsonServerName.toString() : strAddress;
279280

280281
pClient->Connect ( strAddress, strServerName );
281282

0 commit comments

Comments
 (0)