From 69f5b89d5685beb7778b1ded96dc57066c82e9d2 Mon Sep 17 00:00:00 2001 From: David Onuoha <100872605+davidonuoha@users.noreply.github.com> Date: Fri, 30 Oct 2026 20:23:38 +0000 Subject: [PATCH] client: report stream lifecycle RPC failures as error codes The SoapySDR stream API reports failures through the return value: deactivateStream() is documented as "0 for success or error code on failure" and closeStream() returns void. These calls are also made from places that cannot handle an exception: - deactivateStream() is called from the destructor of gr::block_executor (GNU Radio), which is implicitly noexcept. The client throws std::runtime_error when the control connection is gone ("Connection reset by peer", or EOF when the server went away), so losing the remote link aborts the whole host process instead of reporting a dead device. This is gqrx-sdr/gqrx#1445: terminate reached from thread id: ... Got std::runtime_error SoapyRPCUnpacker::recv(header) FAIL: recv() [104: Connection reset by peer] ... SoapyRemoteDevice::deactivateStream(...) [clone .cold]+0x2b ... gr::block_executor::~block_executor()+0x2b2 - activateStream() is called from the block_executor constructor on the block threads. An exception there leaves the scheduler's start barrier unsatisfied, so the host process deadlocks in top_block::unlock() on the next flow graph reconfiguration instead of just failing. - closeStream() is called from the destructor of the client device, and returns void, so it has no way to report such a failure at all. Also release the local stream data unconditionally, so a failed remote call no longer leaks the endpoint. Log the transport error through SoapySDR's logger and report SOAPY_SDR_STREAM_ERROR, so that a lost link is visible as a device error. setupStream() keeps throwing, as documented by SoapySDR. Fixes: gqrx-sdr/gqrx#1445 --- client/Streaming.cpp | 93 ++++++++++++++++++++++++++++++-------------- 1 file changed, 64 insertions(+), 29 deletions(-) diff --git a/client/Streaming.cpp b/client/Streaming.cpp index 15129f6..5a15b8a 100644 --- a/client/Streaming.cpp +++ b/client/Streaming.cpp @@ -13,6 +13,7 @@ #include #include //std::min, std::find #include //unique_ptr +#include //std::exception std::vector SoapyRemoteDevice::__getRemoteOnlyStreamFormats(const int direction, const size_t channel) const { @@ -330,13 +331,23 @@ void SoapyRemoteDevice::closeStream(SoapySDR::Stream *stream) { auto data = (ClientStreamData *)stream; - std::lock_guard lock(_mutex); - SoapyRPCPacker packer(_sock); - packer & SOAPY_REMOTE_CLOSE_STREAM; - packer & data->streamId; - packer(); + //closeStream() returns void, so a failure of the remote call can only be + //reported through the logger. This call is made from destructors (see + //~soapy_source_c() in gr-osmosdr), where an exception would terminate. + try + { + std::lock_guard lock(_mutex); + SoapyRPCPacker packer(_sock); + packer & SOAPY_REMOTE_CLOSE_STREAM; + packer & data->streamId; + packer(); - SoapyRPCUnpacker unpacker(_sock); + SoapyRPCUnpacker unpacker(_sock); + } + catch (const std::exception &ex) + { + SoapySDR::logf(SOAPY_SDR_ERROR, "SoapyRemoteDevice::closeStream() FAIL: %s", ex.what()); + } //cleanup local stream data delete data->endpoint; @@ -358,19 +369,31 @@ int SoapyRemoteDevice::activateStream( { auto data = (ClientStreamData *)stream; - std::lock_guard lock(_mutex); - SoapyRPCPacker packer(_sock); - packer & SOAPY_REMOTE_ACTIVATE_STREAM; - packer & data->streamId; - packer & flags; - packer & timeNs; - packer & int(numElems); - packer(); + //The stream API reports failures through its return value + //(see SoapySDR/Device.hpp). This call is made from the constructor of + //gr::block_executor (GNU Radio), where an exception on the block thread + //leaves the scheduler barrier unsatisfied (deadlock) instead of failing. + try + { + std::lock_guard lock(_mutex); + SoapyRPCPacker packer(_sock); + packer & SOAPY_REMOTE_ACTIVATE_STREAM; + packer & data->streamId; + packer & flags; + packer & timeNs; + packer & int(numElems); + packer(); - SoapyRPCUnpacker unpacker(_sock); - int result = 0; - unpacker & result; - return result; + SoapyRPCUnpacker unpacker(_sock); + int result = 0; + unpacker & result; + return result; + } + catch (const std::exception &ex) + { + SoapySDR::logf(SOAPY_SDR_ERROR, "SoapyRemoteDevice::activateStream() FAIL: %s", ex.what()); + return SOAPY_SDR_STREAM_ERROR; + } } int SoapyRemoteDevice::deactivateStream( @@ -380,18 +403,30 @@ int SoapyRemoteDevice::deactivateStream( { auto data = (ClientStreamData *)stream; - std::lock_guard lock(_mutex); - SoapyRPCPacker packer(_sock); - packer & SOAPY_REMOTE_DEACTIVATE_STREAM; - packer & data->streamId; - packer & flags; - packer & timeNs; - packer(); + //The stream API reports failures through its return value + //(see SoapySDR/Device.hpp). This call is made from the destructor of + //gr::block_executor (GNU Radio), which is noexcept: an exception thrown + //here aborts the whole application instead of returning an error code. + try + { + std::lock_guard lock(_mutex); + SoapyRPCPacker packer(_sock); + packer & SOAPY_REMOTE_DEACTIVATE_STREAM; + packer & data->streamId; + packer & flags; + packer & timeNs; + packer(); - SoapyRPCUnpacker unpacker(_sock); - int result = 0; - unpacker & result; - return result; + SoapyRPCUnpacker unpacker(_sock); + int result = 0; + unpacker & result; + return result; + } + catch (const std::exception &ex) + { + SoapySDR::logf(SOAPY_SDR_ERROR, "SoapyRemoteDevice::deactivateStream() FAIL: %s", ex.what()); + return SOAPY_SDR_STREAM_ERROR; + } } int SoapyRemoteDevice::readStream(