From 13b4f592150f3da47a940edd8361eefa2509d274 Mon Sep 17 00:00:00 2001 From: Jefferson Amstutz Date: Wed, 4 Feb 2026 12:51:30 -0600 Subject: [PATCH 01/11] more robust server state transitions --- .../demos/network/server/RenderServer.cpp | 34 +++++++++---------- .../demos/network/server/RenderServer.hpp | 4 +-- tsd/src/tsd/network/NetworkChannel.cpp | 7 ++++ tsd/src/tsd/network/NetworkChannel.hpp | 1 + 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/tsd/apps/interactive/demos/network/server/RenderServer.cpp b/tsd/apps/interactive/demos/network/server/RenderServer.cpp index 1daccd8f..a4898750 100644 --- a/tsd/apps/interactive/demos/network/server/RenderServer.cpp +++ b/tsd/apps/interactive/demos/network/server/RenderServer.cpp @@ -41,24 +41,26 @@ void RenderServer::run(short port) tsd::core::logStatus("[Server] Listening on port %i...", int(port)); - while (m_mode != ServerMode::SHUTDOWN) { - if (!m_server->isConnected() || m_mode == ServerMode::DISCONNECTED) { + while (m_currentMode != ServerMode::SHUTDOWN) { + bool wasRendering = m_currentMode == ServerMode::RENDERING; + + m_currentMode = + m_server->isConnected() ? m_nextMode : ServerMode::DISCONNECTED; + + if (m_currentMode == ServerMode::DISCONNECTED) { + m_lastSentFrame = {}; // reset any pending frame sends if (m_previousMode != ServerMode::DISCONNECTED) { tsd::core::logStatus("[Server] Listening on port %i...", int(port)); - if (m_lastSentFrame.valid()) - m_lastSentFrame.get(); - m_mode = ServerMode::DISCONNECTED; + m_server->restart(); } - m_wasRenderingBeforeSendScene = false; std::this_thread::sleep_for(std::chrono::seconds(1)); - } else if (m_mode == ServerMode::RENDERING) { + } else if (m_currentMode == ServerMode::RENDERING) { tsd::core::logDebug("[Server] Rendering frame..."); update_FrameConfig(); update_View(); m_renderPipeline.render(); send_FrameBuffer(); - m_wasRenderingBeforeSendScene = true; - } else if (m_mode == ServerMode::SEND_SCENE) { + } else if (m_currentMode == ServerMode::SEND_SCENE) { tsd::core::logStatus("[Server] Serializing + sending scene..."); tsd::core::Timer timer; @@ -69,18 +71,14 @@ void RenderServer::run(short port) timer.end(); tsd::core::logStatus("[Server] ...done! (%.3f s)", timer.seconds()); - m_mode = m_wasRenderingBeforeSendScene ? ServerMode::RENDERING - : ServerMode::PAUSED; + set_Mode(wasRendering ? ServerMode::RENDERING : ServerMode::PAUSED); } else { if (m_previousMode != ServerMode::PAUSED) tsd::core::logStatus("[Server] Rendering paused..."); - m_wasRenderingBeforeSendScene = false; - if (m_lastSentFrame.valid()) - m_lastSentFrame.get(); std::this_thread::sleep_for(std::chrono::seconds(1)); } - m_previousMode = m_mode; + m_previousMode = m_currentMode; } tsd::core::logStatus("[Server] Shutting down..."); @@ -364,9 +362,11 @@ void RenderServer::send_FrameBuffer() void RenderServer::set_Mode(ServerMode mode) { - if (m_mode == ServerMode::SHUTDOWN) // if shutting down, do not change mode + const bool shuttingDown = m_nextMode == ServerMode::SHUTDOWN + || m_currentMode == ServerMode::SHUTDOWN; + if (shuttingDown) // if shutting down, do not change mode return; - m_mode = mode; + m_nextMode = mode; } } // namespace tsd::network diff --git a/tsd/apps/interactive/demos/network/server/RenderServer.hpp b/tsd/apps/interactive/demos/network/server/RenderServer.hpp index 6d57b721..b4bdc431 100644 --- a/tsd/apps/interactive/demos/network/server/RenderServer.hpp +++ b/tsd/apps/interactive/demos/network/server/RenderServer.hpp @@ -61,9 +61,9 @@ struct RenderServer tsd::rendering::Manipulator m_manipulator; tsd::rendering::RenderIndex *m_renderIndex{nullptr}; tsd::rendering::RenderPipeline m_renderPipeline; - ServerMode m_mode{ServerMode::DISCONNECTED}; + ServerMode m_currentMode{ServerMode::DISCONNECTED}; + ServerMode m_nextMode{ServerMode::DISCONNECTED}; ServerMode m_previousMode{ServerMode::DISCONNECTED}; - bool m_wasRenderingBeforeSendScene{false}; struct SessionVersions { diff --git a/tsd/src/tsd/network/NetworkChannel.cpp b/tsd/src/tsd/network/NetworkChannel.cpp index b2ef34a1..756fc125 100644 --- a/tsd/src/tsd/network/NetworkChannel.cpp +++ b/tsd/src/tsd/network/NetworkChannel.cpp @@ -232,6 +232,13 @@ void NetworkServer::start() start_messaging(); } +void NetworkServer::restart() +{ + stop(); + start_accept(); + start(); +} + void NetworkServer::stop() { stop_messaging(); diff --git a/tsd/src/tsd/network/NetworkChannel.hpp b/tsd/src/tsd/network/NetworkChannel.hpp index 21b020fd..f9c60555 100644 --- a/tsd/src/tsd/network/NetworkChannel.hpp +++ b/tsd/src/tsd/network/NetworkChannel.hpp @@ -57,6 +57,7 @@ struct NetworkServer : public NetworkChannel ~NetworkServer() override = default; void start(); + void restart(); // must be running already void stop(); private: From fc7457daeb174b1a10d9c4f13272ac413928bdd2 Mon Sep 17 00:00:00 2001 From: Jefferson Amstutz Date: Wed, 4 Feb 2026 13:06:28 -0600 Subject: [PATCH 02/11] cleanup client disconnection --- .../demos/network/RenderSession.hpp | 3 +++ .../demos/network/client/tsdRemoteViewer.cpp | 19 +++++++------------ .../demos/network/client/tsdTestClient.cpp | 10 +++++----- .../demos/network/server/RenderServer.cpp | 8 +++++++- 4 files changed, 22 insertions(+), 18 deletions(-) diff --git a/tsd/apps/interactive/demos/network/RenderSession.hpp b/tsd/apps/interactive/demos/network/RenderSession.hpp index 9bc7b9de..feeacc4c 100644 --- a/tsd/apps/interactive/demos/network/RenderSession.hpp +++ b/tsd/apps/interactive/demos/network/RenderSession.hpp @@ -45,6 +45,9 @@ enum MessageType // All ping messages PING, + // All disconnections + DISCONNECT, + // All errors ERROR = 255 }; diff --git a/tsd/apps/interactive/demos/network/client/tsdRemoteViewer.cpp b/tsd/apps/interactive/demos/network/client/tsdRemoteViewer.cpp index c33b4a7d..b43ebae2 100644 --- a/tsd/apps/interactive/demos/network/client/tsdRemoteViewer.cpp +++ b/tsd/apps/interactive/demos/network/client/tsdRemoteViewer.cpp @@ -172,22 +172,19 @@ void Application::uiMainMenuBar() if (ImGui::BeginMenu("Server")) { if (ImGui::MenuItem("Start Rendering")) { tsd::core::logStatus("[Client] Sending START_RENDERING command"); - m_client->send( - tsd::network::make_message(MessageType::SERVER_START_RENDERING)); + m_client->send(MessageType::SERVER_START_RENDERING); } if (ImGui::MenuItem("Pause Rendering")) { tsd::core::logStatus("[Client] Sending STOP_RENDERING command"); - m_client->send( - tsd::network::make_message(MessageType::SERVER_STOP_RENDERING)); + m_client->send(MessageType::SERVER_STOP_RENDERING); } ImGui::Separator(); if (ImGui::MenuItem("Shutdown")) { tsd::core::logStatus("[Client] Sending SHUTDOWN command"); - m_client->send(tsd::network::make_message(MessageType::SERVER_SHUTDOWN)) - .get(); + m_client->send(MessageType::SERVER_SHUTDOWN).get(); disconnect(); } @@ -200,7 +197,7 @@ void Application::uiMainMenuBar() if (ImGui::IsKeyPressed(ImGuiKey_P, false)) { tsd::core::logStatus("[Client] Sending PING"); - m_client->send(tsd::network::make_message(MessageType::PING)); + m_client->send(MessageType::PING); } if (ImGui::IsKeyPressed(ImGuiKey_F1, false)) { @@ -325,9 +322,7 @@ void Application::connect() tsd::core::logStatus( "[Client] Connected to server at %s:%d", m_host.c_str(), m_port); tsd::core::logStatus("[Client] Requesting scene from server"); - m_client->send( - tsd::network::make_message(MessageType::SERVER_REQUEST_SCENE)) - .get(); + m_client->send(MessageType::SERVER_REQUEST_SCENE).get(); } else { tsd::core::logError("[Client] Failed to connect to server at %s:%d", m_host.c_str(), @@ -338,8 +333,8 @@ void Application::connect() void Application::disconnect() { tsd::core::logStatus("[Client] Disconnecting from server..."); - m_client->send(tsd::network::make_message(MessageType::SERVER_STOP_RENDERING)) - .get(); + m_updateDelegate->setEnabled(false); + m_client->send(MessageType::DISCONNECT).get(); m_client->disconnect(); auto &scene = appCore()->tsd.scene; diff --git a/tsd/apps/interactive/demos/network/client/tsdTestClient.cpp b/tsd/apps/interactive/demos/network/client/tsdTestClient.cpp index f9345a42..8992744d 100644 --- a/tsd/apps/interactive/demos/network/client/tsdTestClient.cpp +++ b/tsd/apps/interactive/demos/network/client/tsdTestClient.cpp @@ -85,14 +85,14 @@ int main() client->connect("127.0.0.1", 12345); - client->send(make_message(MessageType::SERVER_START_RENDERING)); + client->send(MessageType::SERVER_START_RENDERING); tsd::core::logStatus("[Client] Rendering for 1 second..."); std::this_thread::sleep_for(std::chrono::seconds(1)); - client->send(make_message(MessageType::SERVER_STOP_RENDERING)).get(); - client->send(make_message(MessageType::SERVER_REQUEST_FRAME_CONFIG)).get(); - client->send(make_message(MessageType::SERVER_REQUEST_VIEW)).get(); + client->send(MessageType::SERVER_STOP_RENDERING).get(); + client->send(MessageType::SERVER_REQUEST_FRAME_CONFIG).get(); + client->send(MessageType::SERVER_REQUEST_VIEW).get(); for (int i = 0; i < 3; ++i) { tsd::core::logStatus("[Client] Sending PING #%d", i + 1); @@ -106,7 +106,7 @@ int main() // Shutdown // tsd::core::logStatus("[Client] Sending SHUTDOWN"); - client->send(make_message(MessageType::SERVER_SHUTDOWN)).get(); + client->send(MessageType::SERVER_SHUTDOWN).get(); client->disconnect(); // Store fine frame // diff --git a/tsd/apps/interactive/demos/network/server/RenderServer.cpp b/tsd/apps/interactive/demos/network/server/RenderServer.cpp index a4898750..8aa19049 100644 --- a/tsd/apps/interactive/demos/network/server/RenderServer.cpp +++ b/tsd/apps/interactive/demos/network/server/RenderServer.cpp @@ -189,6 +189,12 @@ void RenderServer::setup_Messaging() tsd::core::logStatus("[Server] Received PING from client"); }); + m_server->registerHandler( + MessageType::DISCONNECT, [&](const tsd::network::Message &msg) { + tsd::core::logStatus("[Server] Client signaled disconnection."); + set_Mode(ServerMode::DISCONNECTED); + }); + m_server->registerHandler(MessageType::SERVER_START_RENDERING, [&](const tsd::network::Message &msg) { tsd::core::logStatus( @@ -310,7 +316,7 @@ void RenderServer::setup_Messaging() [this](const tsd::network::Message &msg) { tsd::core::logDebug("[Server] Client requested scene..."); // Notify client a big message is coming... - m_server->send(make_message(MessageType::CLIENT_SCENE_TRANSFER_BEGIN)); + m_server->send(MessageType::CLIENT_SCENE_TRANSFER_BEGIN); set_Mode(ServerMode::SEND_SCENE); }); } From 5a6ab238bf144ade33a0cb421ed8574694dd0773 Mon Sep 17 00:00:00 2001 From: Jefferson Amstutz Date: Wed, 4 Feb 2026 13:33:44 -0600 Subject: [PATCH 03/11] cleanup NetworkChannel API --- .../demos/network/client/RemoteViewport.cpp | 12 ++-- .../demos/network/client/tsdRemoteViewer.cpp | 3 +- .../demos/network/client/tsdTestClient.cpp | 2 +- .../demos/network/server/RenderServer.cpp | 12 ++-- tsd/src/tsd/network/Message.hpp | 39 ----------- tsd/src/tsd/network/NetworkChannel.cpp | 11 +++- tsd/src/tsd/network/NetworkChannel.hpp | 66 ++++++++++++++++++- 7 files changed, 83 insertions(+), 62 deletions(-) diff --git a/tsd/apps/interactive/demos/network/client/RemoteViewport.cpp b/tsd/apps/interactive/demos/network/client/RemoteViewport.cpp index c2697841..231d47ac 100644 --- a/tsd/apps/interactive/demos/network/client/RemoteViewport.cpp +++ b/tsd/apps/interactive/demos/network/client/RemoteViewport.cpp @@ -42,10 +42,8 @@ void RemoteViewport::buildUI() if (m_viewportSize != viewportSize || m_wasConnected != isConnected) reshape(viewportSize); - if (!m_wasConnected && isConnected) { - m_channel->send( - tsd::network::make_message(MessageType::SERVER_REQUEST_VIEW)); - } + if (!m_wasConnected && isConnected) + m_channel->send(MessageType::SERVER_REQUEST_VIEW); m_wasConnected = isConnected; m_incomingFramePass->setEnabled(isConnected); @@ -136,8 +134,7 @@ void RemoteViewport::reshape(tsd::math::int2 newSize) if (m_channel && m_channel->isConnected()) { tsd::network::RenderSession::Frame::Config frameConfig; frameConfig.size = tsd::math::uint2(newSize.x, newSize.y); - m_channel->send(tsd::network::make_message( - MessageType::SERVER_SET_FRAME_CONFIG, &frameConfig)); + m_channel->send(MessageType::SERVER_SET_FRAME_CONFIG, &frameConfig); } } @@ -153,8 +150,7 @@ void RemoteViewport::updateCamera() viewMsg.azeldist.z = m_arcball->distance(); viewMsg.lookat = m_arcball->at(); - m_channel->send( - tsd::network::make_message(MessageType::SERVER_SET_VIEW, &viewMsg)); + m_channel->send(MessageType::SERVER_SET_VIEW, &viewMsg); } } diff --git a/tsd/apps/interactive/demos/network/client/tsdRemoteViewer.cpp b/tsd/apps/interactive/demos/network/client/tsdRemoteViewer.cpp index b43ebae2..b2381bf5 100644 --- a/tsd/apps/interactive/demos/network/client/tsdRemoteViewer.cpp +++ b/tsd/apps/interactive/demos/network/client/tsdRemoteViewer.cpp @@ -95,8 +95,7 @@ Application::Application() tsd::core::logStatus( "\n%s", tsd::core::objectDBInfo(scene.objectDB()).c_str()); tsd::core::logStatus("[Client] Requesting start of rendering..."); - m_client->send( - tsd::network::make_message(MessageType::SERVER_START_RENDERING)); + m_client->send(MessageType::SERVER_START_RENDERING); }); core->tsd.scene.setUpdateDelegate(m_updateDelegate.get()); diff --git a/tsd/apps/interactive/demos/network/client/tsdTestClient.cpp b/tsd/apps/interactive/demos/network/client/tsdTestClient.cpp index 8992744d..df3a2693 100644 --- a/tsd/apps/interactive/demos/network/client/tsdTestClient.cpp +++ b/tsd/apps/interactive/demos/network/client/tsdTestClient.cpp @@ -97,7 +97,7 @@ int main() for (int i = 0; i < 3; ++i) { tsd::core::logStatus("[Client] Sending PING #%d", i + 1); std::this_thread::sleep_for(std::chrono::seconds(1)); - client->send(make_message(MessageType::PING)); + client->send(MessageType::PING); } tsd::core::logStatus("[Client] Waiting for 3 seconds..."); diff --git a/tsd/apps/interactive/demos/network/server/RenderServer.cpp b/tsd/apps/interactive/demos/network/server/RenderServer.cpp index 8aa19049..49f9c558 100644 --- a/tsd/apps/interactive/demos/network/server/RenderServer.cpp +++ b/tsd/apps/interactive/demos/network/server/RenderServer.cpp @@ -302,14 +302,14 @@ void RenderServer::setup_Messaging() m_server->registerHandler(MessageType::SERVER_REQUEST_FRAME_CONFIG, [s = m_server, session = &m_session](const tsd::network::Message &msg) { tsd::core::logDebug("[Server] Client requested frame config."); - s->send(make_message( - MessageType::CLIENT_RECEIVE_FRAME_CONFIG, &session->frame.config)); + s->send( + MessageType::CLIENT_RECEIVE_FRAME_CONFIG, &session->frame.config); }); m_server->registerHandler(MessageType::SERVER_REQUEST_VIEW, [s = m_server, session = &m_session](const tsd::network::Message &msg) { tsd::core::logDebug("[Server] Client requested view."); - s->send(make_message(MessageType::CLIENT_RECEIVE_VIEW, &session->view)); + s->send(MessageType::CLIENT_RECEIVE_VIEW, &session->view); }); m_server->registerHandler(MessageType::SERVER_REQUEST_SCENE, @@ -361,9 +361,9 @@ void RenderServer::send_FrameBuffer() return; } - m_lastSentFrame = m_server->send( - tsd::network::make_message(MessageType::CLIENT_RECEIVE_FRAME_BUFFER_COLOR, - m_session.frame.buffers.color)); + m_lastSentFrame = + m_server->send(MessageType::CLIENT_RECEIVE_FRAME_BUFFER_COLOR, + m_session.frame.buffers.color); } void RenderServer::set_Mode(ServerMode mode) diff --git a/tsd/src/tsd/network/Message.hpp b/tsd/src/tsd/network/Message.hpp index b1e18740..6f28004f 100644 --- a/tsd/src/tsd/network/Message.hpp +++ b/tsd/src/tsd/network/Message.hpp @@ -79,15 +79,6 @@ bool payloadRead( const Message &msg, uint32_t &offset, T *data, uint32_t length = 1); bool payloadRead(const Message &msg, uint32_t &offset, std::string &str); -// make_message() // - -Message make_message(uint8_t type); -Message make_message(uint8_t type, const std::string &data); -template -Message make_message(uint8_t type, const T *data, uint32_t count = 1); -template -Message make_message(uint8_t type, const std::vector &data); - /////////////////////////////////////////////////////////////////////////////// // Inlined definitions //////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// @@ -179,34 +170,4 @@ inline bool payloadRead(const Message &msg, uint32_t &offset, std::string &str) return true; } -inline Message make_message(uint8_t type) -{ - Message msg; - msg.header.type = type; - return msg; -} - -inline Message make_message(uint8_t type, const std::string &data) -{ - Message msg = make_message(type); - payloadWrite(msg, data); - return msg; -} - -template -inline Message make_message(uint8_t type, const T *data, uint32_t count) -{ - Message msg = make_message(type); - payloadWrite(msg, data, count); - return msg; -} - -template -inline Message make_message(uint8_t type, const std::vector &data) -{ - static_assert(std::is_standard_layout_v && std::is_trivially_copyable_v, - "Message payload must be a POD type"); - return make_message(type, data.data(), static_cast(data.size())); -} - } // namespace tsd::network diff --git a/tsd/src/tsd/network/NetworkChannel.cpp b/tsd/src/tsd/network/NetworkChannel.cpp index 756fc125..4db6fb7f 100644 --- a/tsd/src/tsd/network/NetworkChannel.cpp +++ b/tsd/src/tsd/network/NetworkChannel.cpp @@ -86,15 +86,20 @@ MessageFuture NetworkChannel::send(Message &&msg) return future; } +MessageFuture NetworkChannel::send(uint8_t type, StructuredMessage &&msg) +{ + Message message = msg.toMessage(type); + return send(std::move(message)); +} + MessageFuture NetworkChannel::send(uint8_t type) { return send(make_message(type)); } -MessageFuture NetworkChannel::send(uint8_t type, StructuredMessage &&msg) +MessageFuture NetworkChannel::send(uint8_t type, const std::string &str) { - Message message = msg.toMessage(type); - return send(std::move(message)); + return send(make_message(type, str)); } void NetworkChannel::start_messaging() diff --git a/tsd/src/tsd/network/NetworkChannel.hpp b/tsd/src/tsd/network/NetworkChannel.hpp index f9c60555..1f650e41 100644 --- a/tsd/src/tsd/network/NetworkChannel.hpp +++ b/tsd/src/tsd/network/NetworkChannel.hpp @@ -20,18 +20,27 @@ struct NetworkChannel : public std::enable_shared_from_this bool isConnected() const; - // Receive messages // + //// Receive messages //// void registerHandler(uint8_t messageType, MessageHandler handler); void removeHandler(uint8_t messageType); void removeAllHandlers(); - // Send messages // + //// Send messages //// MessageFuture send(Message &&msg); - MessageFuture send(uint8_t type); // empty payload MessageFuture send(uint8_t type, StructuredMessage &&msg); + /* No payload */ + MessageFuture send(uint8_t type); + + /* With payloads */ + MessageFuture send(uint8_t type, const std::string &str); + template + MessageFuture send(uint8_t type, const T *data, uint32_t count = 1); + template + MessageFuture send(uint8_t type, const std::vector &data); + protected: void start_messaging(); void stop_messaging(); @@ -49,6 +58,12 @@ struct NetworkChannel : public std::enable_shared_from_this tcp::socket m_socket; Message m_currentMessage; HandlerMap m_handlers; + + private: + Message make_message(uint8_t type); + Message make_message(uint8_t type, const std::string &data); + template + Message make_message(uint8_t type, const T *data, uint32_t count); }; struct NetworkServer : public NetworkChannel @@ -76,6 +91,51 @@ struct NetworkClient : public NetworkChannel void disconnect(); }; +// Inline definitions ///////////////////////////////////////////////////////// + +template +inline MessageFuture NetworkChannel::send( + uint8_t type, const T *data, uint32_t count) +{ + static_assert(std::is_standard_layout_v && std::is_trivially_copyable_v, + "Message payload must be a POD type"); + return send(make_message(type, data, count)); +} + +template +inline MessageFuture NetworkChannel::send( + uint8_t type, const std::vector &data) +{ + return send( + make_message(type, data.data(), static_cast(data.size()))); +} + +inline Message NetworkChannel::make_message(uint8_t type) +{ + Message msg; + msg.header.type = type; + return msg; +} + +inline Message NetworkChannel::make_message( + uint8_t type, const std::string &data) +{ + Message msg = make_message(type); + payloadWrite(msg, data); + return msg; +} + +template +inline Message NetworkChannel::make_message( + uint8_t type, const T *data, uint32_t count) +{ + static_assert(std::is_standard_layout_v && std::is_trivially_copyable_v, + "Message payload must be a POD type"); + Message msg = make_message(type); + payloadWrite(msg, data, count); + return msg; +} + // Inlined helper functions /////////////////////////////////////////////////// template From faa0506d4fc5c36b8a280c03662c60567e6cdd2e Mon Sep 17 00:00:00 2001 From: Jefferson Amstutz Date: Wed, 4 Feb 2026 13:43:03 -0600 Subject: [PATCH 04/11] make all NetworkUpdateDelegate messages synchronous --- .../network/client/NetworkUpdateDelegate.cpp | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tsd/apps/interactive/demos/network/client/NetworkUpdateDelegate.cpp b/tsd/apps/interactive/demos/network/client/NetworkUpdateDelegate.cpp index 366e2806..cd9be3ad 100644 --- a/tsd/apps/interactive/demos/network/client/NetworkUpdateDelegate.cpp +++ b/tsd/apps/interactive/demos/network/client/NetworkUpdateDelegate.cpp @@ -44,7 +44,7 @@ void NetworkUpdateDelegate::signalObjectAdded(const tsd::core::Object *o) return; } auto msg = tsd::network::messages::NewObject(o); - m_channel->send(MessageType::SERVER_ADD_OBJECT, std::move(msg)); + m_channel->send(MessageType::SERVER_ADD_OBJECT, std::move(msg)).get(); } void NetworkUpdateDelegate::signalParameterUpdated( @@ -58,7 +58,8 @@ void NetworkUpdateDelegate::signalParameterUpdated( return; } auto msg = tsd::network::messages::ParameterChange(o, p); - m_channel->send(MessageType::SERVER_SET_OBJECT_PARAMETER, std::move(msg)); + m_channel->send(MessageType::SERVER_SET_OBJECT_PARAMETER, std::move(msg)) + .get(); } void NetworkUpdateDelegate::signalParameterRemoved( @@ -72,7 +73,8 @@ void NetworkUpdateDelegate::signalParameterRemoved( return; } auto msg = tsd::network::messages::ParameterRemove(o, p); - m_channel->send(MessageType::SERVER_REMOVE_OBJECT_PARAMETER, std::move(msg)); + m_channel->send(MessageType::SERVER_REMOVE_OBJECT_PARAMETER, std::move(msg)) + .get(); } void NetworkUpdateDelegate::signalArrayMapped(const tsd::core::Array *) @@ -92,7 +94,7 @@ void NetworkUpdateDelegate::signalArrayUnmapped(const tsd::core::Array *a) return; } auto msg = tsd::network::messages::TransferArrayData(a); - m_channel->send(MessageType::SERVER_SET_ARRAY_DATA, std::move(msg)); + m_channel->send(MessageType::SERVER_SET_ARRAY_DATA, std::move(msg)).get(); } void NetworkUpdateDelegate::signalObjectParameterUseCountZero( @@ -121,7 +123,7 @@ void NetworkUpdateDelegate::signalObjectRemoved(const tsd::core::Object *o) return; } auto msg = tsd::network::messages::RemoveObject(o); - m_channel->send(MessageType::SERVER_REMOVE_OBJECT, std::move(msg)); + m_channel->send(MessageType::SERVER_REMOVE_OBJECT, std::move(msg)).get(); } void NetworkUpdateDelegate::signalRemoveAllObjects() @@ -133,7 +135,7 @@ void NetworkUpdateDelegate::signalRemoveAllObjects() "NetworkUpdateDelegate::signalRemoveAllObjects: no network channel"); return; } - m_channel->send(MessageType::SERVER_REMOVE_ALL_OBJECTS); + m_channel->send(MessageType::SERVER_REMOVE_ALL_OBJECTS).get(); } void NetworkUpdateDelegate::signalLayerAdded(const tsd::core::Layer *l) @@ -147,7 +149,7 @@ void NetworkUpdateDelegate::signalLayerAdded(const tsd::core::Layer *l) } auto msg = tsd::network::messages::TransferLayer( m_scene, const_cast(l)); - m_channel->send(MessageType::SERVER_UPDATE_LAYER, std::move(msg)); + m_channel->send(MessageType::SERVER_UPDATE_LAYER, std::move(msg)).get(); } void NetworkUpdateDelegate::signalLayerUpdated(const tsd::core::Layer *l) @@ -161,7 +163,7 @@ void NetworkUpdateDelegate::signalLayerUpdated(const tsd::core::Layer *l) } auto msg = tsd::network::messages::TransferLayer( m_scene, const_cast(l)); - m_channel->send(MessageType::SERVER_UPDATE_LAYER, std::move(msg)); + m_channel->send(MessageType::SERVER_UPDATE_LAYER, std::move(msg)).get(); } void NetworkUpdateDelegate::signalLayerRemoved(const tsd::core::Layer *) From e26cc8a1dda212c9987e37c8dacfa0688a5886c1 Mon Sep 17 00:00:00 2001 From: Jefferson Amstutz Date: Wed, 4 Feb 2026 13:47:55 -0600 Subject: [PATCH 05/11] more robust client disconnection --- tsd/src/tsd/network/NetworkChannel.cpp | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/tsd/src/tsd/network/NetworkChannel.cpp b/tsd/src/tsd/network/NetworkChannel.cpp index 4db6fb7f..0ad18c0a 100644 --- a/tsd/src/tsd/network/NetworkChannel.cpp +++ b/tsd/src/tsd/network/NetworkChannel.cpp @@ -291,20 +291,8 @@ void NetworkClient::connect(const std::string &host, short port) void NetworkClient::disconnect() { - boost::system::error_code ec{}; - m_socket.shutdown(tcp::socket::shutdown_both, ec); - if (ec) { - tsd::core::logError( - "[NetworkClient] Shutdown error: %s", ec.message().c_str()); - } - m_socket.close(ec); - if (ec) { - tsd::core::logError( - "[NetworkClient] Close error: %s", ec.message().c_str()); - } else { - tsd::core::logStatus("[NetworkClient] Disconnected from server"); - } stop_messaging(); + tsd::core::logStatus("[NetworkClient] Disconnected from server"); } } // namespace tsd::network From 03c806b2138829484238db9e22ff375db5365a50 Mon Sep 17 00:00:00 2001 From: Jefferson Amstutz Date: Wed, 4 Feb 2026 13:59:55 -0600 Subject: [PATCH 06/11] make sure the LayerTree is only active when appropriate --- .../interactive/demos/network/client/tsdRemoteViewer.cpp | 7 +++++-- tsd/src/tsd/ui/imgui/windows/LayerTree.cpp | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/tsd/apps/interactive/demos/network/client/tsdRemoteViewer.cpp b/tsd/apps/interactive/demos/network/client/tsdRemoteViewer.cpp index b2381bf5..548883b7 100644 --- a/tsd/apps/interactive/demos/network/client/tsdRemoteViewer.cpp +++ b/tsd/apps/interactive/demos/network/client/tsdRemoteViewer.cpp @@ -96,9 +96,11 @@ Application::Application() "\n%s", tsd::core::objectDBInfo(scene.objectDB()).c_str()); tsd::core::logStatus("[Client] Requesting start of rendering..."); m_client->send(MessageType::SERVER_START_RENDERING); + appCore()->tsd.sceneLoadComplete = true; }); core->tsd.scene.setUpdateDelegate(m_updateDelegate.get()); + core->tsd.sceneLoadComplete = false; } Application::~Application() = default; @@ -109,7 +111,6 @@ anari_viewer::WindowArray Application::setupWindows() auto *core = appCore(); auto *manipulator = &core->view.manipulator; - core->tsd.sceneLoadComplete = true; auto *log = new tsd_ui::Log(this); m_viewport = @@ -336,7 +337,9 @@ void Application::disconnect() m_client->send(MessageType::DISCONNECT).get(); m_client->disconnect(); - auto &scene = appCore()->tsd.scene; + auto *core = appCore(); + core->tsd.sceneLoadComplete = false; + auto &scene = core->tsd.scene; scene.removeAllLayers(); scene.removeAllObjects(); } diff --git a/tsd/src/tsd/ui/imgui/windows/LayerTree.cpp b/tsd/src/tsd/ui/imgui/windows/LayerTree.cpp index eafdb03c..5796f3f5 100644 --- a/tsd/src/tsd/ui/imgui/windows/LayerTree.cpp +++ b/tsd/src/tsd/ui/imgui/windows/LayerTree.cpp @@ -32,7 +32,7 @@ LayerTree::LayerTree(Application *app, const char *name) : Window(app, name) {} void LayerTree::buildUI() { if (!appCore()->tsd.sceneLoadComplete) { - ImGui::Text("PLEASE WAIT...LOADING SCENE"); + ImGui::Text("{SCENE NOT AVAILABLE}"); return; } From 5055fff54ca4c06eae7408d5f9917026998b73f8 Mon Sep 17 00:00:00 2001 From: Jefferson Amstutz Date: Wed, 4 Feb 2026 16:50:23 -0600 Subject: [PATCH 07/11] ensure message completion handlers are flushed when messaging is stopped --- tsd/src/tsd/network/NetworkChannel.cpp | 11 +++++++---- tsd/src/tsd/network/NetworkChannel.hpp | 5 ++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/tsd/src/tsd/network/NetworkChannel.cpp b/tsd/src/tsd/network/NetworkChannel.cpp index 0ad18c0a..0ea9d7ea 100644 --- a/tsd/src/tsd/network/NetworkChannel.cpp +++ b/tsd/src/tsd/network/NetworkChannel.cpp @@ -17,9 +17,7 @@ static void async_invoke(boost::asio::io_context &io_context, FCN &&f) // NetworkChannel definitions ///////////////////////////////////////////////// -NetworkChannel::NetworkChannel() - : m_work(asio::make_work_guard(m_io_context)), m_socket(m_io_context) -{} +NetworkChannel::NetworkChannel() : m_socket(m_io_context) {} NetworkChannel::~NetworkChannel() { @@ -48,7 +46,8 @@ void NetworkChannel::removeAllHandlers() MessageFuture NetworkChannel::send(Message &&msg) { - auto promise = std::make_shared>(); + using MessagePromise = std::promise; + auto promise = std::make_shared(); auto future = promise->get_future(); if (!isConnected()) { @@ -106,6 +105,7 @@ void NetworkChannel::start_messaging() { tsd::core::logDebug("[NetworkChannel] starting channel"); stop_messaging(); + m_work.emplace(asio::make_work_guard(m_io_context)); m_io_context.restart(); m_io_thread = std::thread([this]() { tsd::core::logDebug("[NetworkChannel] starting IO thread"); @@ -130,6 +130,9 @@ void NetworkChannel::stop_messaging() m_io_context.stop(); if (m_io_thread.joinable()) m_io_thread.join(); + m_work.reset(); + m_io_context.restart(); + m_io_context.poll(); // drain any remaining tasks } catch (const std::system_error &e) { tsd::core::logError( "[NetworkChannel] System error during stop: %s", e.what()); diff --git a/tsd/src/tsd/network/NetworkChannel.hpp b/tsd/src/tsd/network/NetworkChannel.hpp index 1f650e41..735360d4 100644 --- a/tsd/src/tsd/network/NetworkChannel.hpp +++ b/tsd/src/tsd/network/NetworkChannel.hpp @@ -7,6 +7,7 @@ // std #include #include +#include #include namespace tsd::network { @@ -53,7 +54,9 @@ struct NetworkChannel : public std::enable_shared_from_this asio::io_context m_io_context; std::thread m_io_thread; - asio::executor_work_guard m_work; + + using WorkGuard = asio::executor_work_guard; + std::optional m_work; tcp::socket m_socket; Message m_currentMessage; From 0a77d8c6745bc9a89c4b3aa904f0c0c18f29ff26 Mon Sep 17 00:00:00 2001 From: Jefferson Amstutz Date: Wed, 4 Feb 2026 16:57:39 -0600 Subject: [PATCH 08/11] make sure nothing is selected when disconnecting interactive client --- tsd/apps/interactive/demos/network/client/tsdRemoteViewer.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/tsd/apps/interactive/demos/network/client/tsdRemoteViewer.cpp b/tsd/apps/interactive/demos/network/client/tsdRemoteViewer.cpp index 548883b7..9509ec17 100644 --- a/tsd/apps/interactive/demos/network/client/tsdRemoteViewer.cpp +++ b/tsd/apps/interactive/demos/network/client/tsdRemoteViewer.cpp @@ -339,6 +339,7 @@ void Application::disconnect() auto *core = appCore(); core->tsd.sceneLoadComplete = false; + core->clearSelected(); auto &scene = core->tsd.scene; scene.removeAllLayers(); scene.removeAllObjects(); From ed6b4148c1bbf433e424e846ad01218ae873cc65 Mon Sep 17 00:00:00 2001 From: Jefferson Amstutz Date: Wed, 4 Feb 2026 17:02:32 -0600 Subject: [PATCH 09/11] ensure shutdown always occurs when triggered --- tsd/apps/interactive/demos/network/server/RenderServer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsd/apps/interactive/demos/network/server/RenderServer.cpp b/tsd/apps/interactive/demos/network/server/RenderServer.cpp index 49f9c558..7ff3ce28 100644 --- a/tsd/apps/interactive/demos/network/server/RenderServer.cpp +++ b/tsd/apps/interactive/demos/network/server/RenderServer.cpp @@ -41,7 +41,7 @@ void RenderServer::run(short port) tsd::core::logStatus("[Server] Listening on port %i...", int(port)); - while (m_currentMode != ServerMode::SHUTDOWN) { + while (m_nextMode != ServerMode::SHUTDOWN) { bool wasRendering = m_currentMode == ServerMode::RENDERING; m_currentMode = From 6e32a650179c4d8aac42ad07ca82594b132fd6d5 Mon Sep 17 00:00:00 2001 From: Jefferson Amstutz Date: Wed, 4 Feb 2026 20:45:14 -0600 Subject: [PATCH 10/11] fix message lifetime issues --- .../demos/network/server/RenderServer.cpp | 1 - tsd/src/tsd/network/NetworkChannel.cpp | 77 +++++++++++-------- tsd/src/tsd/network/NetworkChannel.hpp | 5 +- 3 files changed, 45 insertions(+), 38 deletions(-) diff --git a/tsd/apps/interactive/demos/network/server/RenderServer.cpp b/tsd/apps/interactive/demos/network/server/RenderServer.cpp index 7ff3ce28..575cc631 100644 --- a/tsd/apps/interactive/demos/network/server/RenderServer.cpp +++ b/tsd/apps/interactive/demos/network/server/RenderServer.cpp @@ -48,7 +48,6 @@ void RenderServer::run(short port) m_server->isConnected() ? m_nextMode : ServerMode::DISCONNECTED; if (m_currentMode == ServerMode::DISCONNECTED) { - m_lastSentFrame = {}; // reset any pending frame sends if (m_previousMode != ServerMode::DISCONNECTED) { tsd::core::logStatus("[Server] Listening on port %i...", int(port)); m_server->restart(); diff --git a/tsd/src/tsd/network/NetworkChannel.cpp b/tsd/src/tsd/network/NetworkChannel.cpp index 0ea9d7ea..94dbc785 100644 --- a/tsd/src/tsd/network/NetworkChannel.cpp +++ b/tsd/src/tsd/network/NetworkChannel.cpp @@ -17,7 +17,10 @@ static void async_invoke(boost::asio::io_context &io_context, FCN &&f) // NetworkChannel definitions ///////////////////////////////////////////////// -NetworkChannel::NetworkChannel() : m_socket(m_io_context) {} +NetworkChannel::NetworkChannel() : m_socket(m_io_context) +{ + m_io_context.stop(); +} NetworkChannel::~NetworkChannel() { @@ -124,15 +127,20 @@ void NetworkChannel::start_messaging() void NetworkChannel::stop_messaging() { try { - boost::system::error_code ec{}; - m_socket.shutdown(tcp::socket::shutdown_both, ec); - m_socket.close(ec); - m_io_context.stop(); - if (m_io_thread.joinable()) - m_io_thread.join(); - m_work.reset(); - m_io_context.restart(); - m_io_context.poll(); // drain any remaining tasks + if (m_socket.is_open()) { + boost::system::error_code ec{}; + m_socket.shutdown(tcp::socket::shutdown_both, ec); + m_socket.close(ec); + } + if (!m_io_context.stopped()) { + m_io_context.stop(); + if (m_io_thread.joinable()) + m_io_thread.join(); + m_work.reset(); + m_io_context.restart(); + m_io_context.poll(); // drain any remaining tasks + m_io_context.stop(); + } } catch (const std::system_error &e) { tsd::core::logError( "[NetworkChannel] System error during stop: %s", e.what()); @@ -148,58 +156,57 @@ void NetworkChannel::read_header() return; } + auto message = std::make_shared(); auto self = shared_from_this(); asio::async_read(m_socket, - asio::buffer(&m_currentMessage.header, sizeof(Message::Header)), - [this, self](const boost::system::error_code &error, + asio::buffer(&message->header, sizeof(Message::Header)), + [this, self, message](const boost::system::error_code &error, std::size_t bytes_transferred) { log_asio_error(error, "ReadHeader"); if (!error) - read_payload(); // Read next message + read_payload(message); // Read next message }); } -void NetworkChannel::read_payload() +void NetworkChannel::read_payload(std::shared_ptr msg) { if (!isConnected()) { tsd::core::logError("[NetworkChannel] Cannot read payload: not connected"); return; } - if (m_currentMessage.header.payload_length == 0) { - async_invoke(m_io_context, [this]() { - invoke_handler(); - read_header(); // Read next message + if (msg->header.payload_length == 0) { + async_invoke(m_io_context, [this, msg]() { + invoke_handler(msg); + read_header(); // Read next msg }); return; } - m_currentMessage.payload.resize(m_currentMessage.header.payload_length); + msg->payload.resize(msg->header.payload_length); auto self = shared_from_this(); asio::async_read(m_socket, - asio::buffer(m_currentMessage.payload.data(), - m_currentMessage.header.payload_length), - [this, self](const boost::system::error_code &error, + asio::buffer(msg->payload.data(), msg->header.payload_length), + [this, self, msg](const boost::system::error_code &error, std::size_t bytes_transferred) { log_asio_error(error, "ReadPayload"); if (!error) { - invoke_handler(); - read_header(); // Read next message + invoke_handler(msg); + read_header(); // Read next msg } }); } -void NetworkChannel::invoke_handler() +void NetworkChannel::invoke_handler(std::shared_ptr msg) { - Message &msg = m_currentMessage; // Invoke handler if registered - if (auto *handler = m_handlers.at(msg.header.type); handler != nullptr) { - (*handler)(msg); + if (auto *handler = m_handlers.at(msg->header.type); handler != nullptr) { + (*handler)(*msg); } else { tsd::core::logWarning( "[NetworkChannel] No handler registered for message type %d", - static_cast(msg.header.type)); + static_cast(msg->header.type)); } } @@ -222,9 +229,11 @@ void NetworkChannel::log_asio_error( "[NetworkChannel] %s error: %s", context, error.message().c_str()); } - boost::system::error_code ec{}; - m_socket.shutdown(tcp::socket::shutdown_both, ec); - m_socket.close(ec); + if (m_socket.is_open()) { + boost::system::error_code ec{}; + m_socket.shutdown(tcp::socket::shutdown_both, ec); + m_socket.close(ec); + } } // NetworkServer definitions ////////////////////////////////////////////////// @@ -261,9 +270,9 @@ void NetworkServer::start_accept() tsd::core::logStatus("[NetworkServer] New connection from %s", socket->remote_endpoint().address().to_string().c_str()); m_socket = std::move(*socket); + read_header(); + start_accept(); // Accept next connection } - read_header(); - start_accept(); // Accept next connection }); } diff --git a/tsd/src/tsd/network/NetworkChannel.hpp b/tsd/src/tsd/network/NetworkChannel.hpp index 735360d4..f376f0e8 100644 --- a/tsd/src/tsd/network/NetworkChannel.hpp +++ b/tsd/src/tsd/network/NetworkChannel.hpp @@ -47,8 +47,8 @@ struct NetworkChannel : public std::enable_shared_from_this void stop_messaging(); void read_header(); - void read_payload(); - void invoke_handler(); + void read_payload(std::shared_ptr msg); + void invoke_handler(std::shared_ptr msg); void log_asio_error( const boost::system::error_code &error, const char *context); @@ -59,7 +59,6 @@ struct NetworkChannel : public std::enable_shared_from_this std::optional m_work; tcp::socket m_socket; - Message m_currentMessage; HandlerMap m_handlers; private: From 8c0cadb27f8e770d0b7369fd6de475ed60204fbf Mon Sep 17 00:00:00 2001 From: Jefferson Amstutz Date: Thu, 5 Feb 2026 08:31:48 -0600 Subject: [PATCH 11/11] address review feedback --- .../network/client/NetworkUpdateDelegate.cpp | 18 ++++++++---------- tsd/src/tsd/network/NetworkChannel.cpp | 11 ++++------- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/tsd/apps/interactive/demos/network/client/NetworkUpdateDelegate.cpp b/tsd/apps/interactive/demos/network/client/NetworkUpdateDelegate.cpp index cd9be3ad..366e2806 100644 --- a/tsd/apps/interactive/demos/network/client/NetworkUpdateDelegate.cpp +++ b/tsd/apps/interactive/demos/network/client/NetworkUpdateDelegate.cpp @@ -44,7 +44,7 @@ void NetworkUpdateDelegate::signalObjectAdded(const tsd::core::Object *o) return; } auto msg = tsd::network::messages::NewObject(o); - m_channel->send(MessageType::SERVER_ADD_OBJECT, std::move(msg)).get(); + m_channel->send(MessageType::SERVER_ADD_OBJECT, std::move(msg)); } void NetworkUpdateDelegate::signalParameterUpdated( @@ -58,8 +58,7 @@ void NetworkUpdateDelegate::signalParameterUpdated( return; } auto msg = tsd::network::messages::ParameterChange(o, p); - m_channel->send(MessageType::SERVER_SET_OBJECT_PARAMETER, std::move(msg)) - .get(); + m_channel->send(MessageType::SERVER_SET_OBJECT_PARAMETER, std::move(msg)); } void NetworkUpdateDelegate::signalParameterRemoved( @@ -73,8 +72,7 @@ void NetworkUpdateDelegate::signalParameterRemoved( return; } auto msg = tsd::network::messages::ParameterRemove(o, p); - m_channel->send(MessageType::SERVER_REMOVE_OBJECT_PARAMETER, std::move(msg)) - .get(); + m_channel->send(MessageType::SERVER_REMOVE_OBJECT_PARAMETER, std::move(msg)); } void NetworkUpdateDelegate::signalArrayMapped(const tsd::core::Array *) @@ -94,7 +92,7 @@ void NetworkUpdateDelegate::signalArrayUnmapped(const tsd::core::Array *a) return; } auto msg = tsd::network::messages::TransferArrayData(a); - m_channel->send(MessageType::SERVER_SET_ARRAY_DATA, std::move(msg)).get(); + m_channel->send(MessageType::SERVER_SET_ARRAY_DATA, std::move(msg)); } void NetworkUpdateDelegate::signalObjectParameterUseCountZero( @@ -123,7 +121,7 @@ void NetworkUpdateDelegate::signalObjectRemoved(const tsd::core::Object *o) return; } auto msg = tsd::network::messages::RemoveObject(o); - m_channel->send(MessageType::SERVER_REMOVE_OBJECT, std::move(msg)).get(); + m_channel->send(MessageType::SERVER_REMOVE_OBJECT, std::move(msg)); } void NetworkUpdateDelegate::signalRemoveAllObjects() @@ -135,7 +133,7 @@ void NetworkUpdateDelegate::signalRemoveAllObjects() "NetworkUpdateDelegate::signalRemoveAllObjects: no network channel"); return; } - m_channel->send(MessageType::SERVER_REMOVE_ALL_OBJECTS).get(); + m_channel->send(MessageType::SERVER_REMOVE_ALL_OBJECTS); } void NetworkUpdateDelegate::signalLayerAdded(const tsd::core::Layer *l) @@ -149,7 +147,7 @@ void NetworkUpdateDelegate::signalLayerAdded(const tsd::core::Layer *l) } auto msg = tsd::network::messages::TransferLayer( m_scene, const_cast(l)); - m_channel->send(MessageType::SERVER_UPDATE_LAYER, std::move(msg)).get(); + m_channel->send(MessageType::SERVER_UPDATE_LAYER, std::move(msg)); } void NetworkUpdateDelegate::signalLayerUpdated(const tsd::core::Layer *l) @@ -163,7 +161,7 @@ void NetworkUpdateDelegate::signalLayerUpdated(const tsd::core::Layer *l) } auto msg = tsd::network::messages::TransferLayer( m_scene, const_cast(l)); - m_channel->send(MessageType::SERVER_UPDATE_LAYER, std::move(msg)).get(); + m_channel->send(MessageType::SERVER_UPDATE_LAYER, std::move(msg)); } void NetworkUpdateDelegate::signalLayerRemoved(const tsd::core::Layer *) diff --git a/tsd/src/tsd/network/NetworkChannel.cpp b/tsd/src/tsd/network/NetworkChannel.cpp index 94dbc785..c4d7e284 100644 --- a/tsd/src/tsd/network/NetworkChannel.cpp +++ b/tsd/src/tsd/network/NetworkChannel.cpp @@ -17,10 +17,7 @@ static void async_invoke(boost::asio::io_context &io_context, FCN &&f) // NetworkChannel definitions ///////////////////////////////////////////////// -NetworkChannel::NetworkChannel() : m_socket(m_io_context) -{ - m_io_context.stop(); -} +NetworkChannel::NetworkChannel() : m_socket(m_io_context) {} NetworkChannel::~NetworkChannel() { @@ -137,9 +134,9 @@ void NetworkChannel::stop_messaging() if (m_io_thread.joinable()) m_io_thread.join(); m_work.reset(); - m_io_context.restart(); - m_io_context.poll(); // drain any remaining tasks - m_io_context.stop(); + + // Ensure all completion handlers have finished before returning + m_io_context.poll(); } } catch (const std::system_error &e) { tsd::core::logError(