LCOV - code coverage report
Current view: top level - wirestead/transport/uds - uds_server.cc (source / functions) Coverage Total Hit
Test: Wirestead Coverage Report Lines: 90.1 % 588 530
Test Date: 2026-10-29 11:36:05 Functions: 93.4 % 76 71
Legend: Lines: hit not hit | Branches: + taken - not taken # not executed Branches: 71.3 % 404 288

             Branch data     Line data    Source code
       1                 :             : /*
       2                 :             :  * Copyright 2025 Jinwoo Sung
       3                 :             :  *
       4                 :             :  * Licensed under the Apache License, Version 2.0 (the "License");
       5                 :             :  * you may not use this file except in compliance with the License.
       6                 :             :  * You may obtain a copy of the License at
       7                 :             :  *
       8                 :             :  *     http://www.apache.org/licenses/LICENSE-2.0
       9                 :             :  *
      10                 :             :  * Unless required by applicable law or agreed to in writing, software
      11                 :             :  * distributed under the License is distributed on an "AS IS" BASIS,
      12                 :             :  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
      13                 :             :  * See the License for the specific language governing permissions and
      14                 :             :  * limitations under the License.
      15                 :             :  */
      16                 :             : 
      17                 :             : #include "wirestead/transport/uds/uds_server.hpp"
      18                 :             : 
      19                 :             : #include <spdlog/fmt/fmt.h>
      20                 :             : 
      21                 :             : #include <algorithm>
      22                 :             : #include <atomic>
      23                 :             : #include <boost/asio.hpp>
      24                 :             : #include <cerrno>
      25                 :             : #include <condition_variable>
      26                 :             : #include <cstdio>
      27                 :             : #include <cstring>
      28                 :             : #include <mutex>
      29                 :             : #include <stop_token>
      30                 :             : #include <string_view>
      31                 :             : #include <thread>
      32                 :             : #include <unordered_map>
      33                 :             : 
      34                 :             : #include "wirestead/base/platform.hpp"
      35                 :             : #include "wirestead/builder/auto_initializer.hpp"
      36                 :             : #include "wirestead/concurrency/io_context_manager.hpp"
      37                 :             : #include "wirestead/concurrency/io_thread_hook.hpp"
      38                 :             : #include "wirestead/concurrency/thread_safe_state.hpp"
      39                 :             : #include "wirestead/config/validation.hpp"
      40                 :             : #include "wirestead/diagnostics/callback.hpp"
      41                 :             : #include "wirestead/diagnostics/logger.hpp"
      42                 :             : #include "wirestead/diagnostics/runtime_stats_counter.hpp"
      43                 :             : #include "wirestead/interface/iuds_acceptor.hpp"
      44                 :             : #include "wirestead/transport/base/error_info_holder.hpp"
      45                 :             : #include "wirestead/transport/base/stop_test_hook.hpp"
      46                 :             : #include "wirestead/transport/uds/boost_uds_acceptor.hpp"
      47                 :             : #include "wirestead/transport/uds/uds_server_session.hpp"
      48                 :             : #include "wirestead/wrapper/send_validation.hpp"
      49                 :             : 
      50                 :             : #if !defined(WIRESTEAD_PLATFORM_WINDOWS)
      51                 :             : #include <sys/socket.h>
      52                 :             : #include <sys/stat.h>
      53                 :             : #include <sys/un.h>
      54                 :             : #include <unistd.h>
      55                 :             : #endif
      56                 :             : 
      57                 :             : namespace wirestead {
      58                 :             : namespace transport {
      59                 :             : 
      60                 :             : namespace net = boost::asio;
      61                 :             : using uds = net::local::stream_protocol;
      62                 :             : 
      63                 :             : namespace {
      64                 :             : 
      65                 :             : // #438: an existing path at the configured socket location should only ever
      66                 :             : // be silently removed if it's a genuinely stale (no longer listened-on)
      67                 :             : // socket file - not a regular file/directory (misconfiguration) and not a
      68                 :             : // socket another live process is still listening on (which would otherwise
      69                 :             : // be silently hijacked instead of failing with a clear error). Returns a
      70                 :             : // non-empty reason string if bind should be refused; empty if it's safe to
      71                 :             : // remove the path (or there's nothing there) and proceed.
      72                 :         194 : std::string existing_uds_path_blocks_bind(const std::string& path) {
      73                 :             : #if defined(WIRESTEAD_PLATFORM_WINDOWS)
      74                 :             :   (void)path;
      75                 :             :   return {};
      76                 :             : #else
      77                 :         194 :   struct stat st {};
      78         [ +  + ]:         194 :   if (::stat(path.c_str(), &st) != 0) {
      79                 :         191 :     return {};  // nothing exists at this path - nothing to check
      80                 :             :   }
      81         [ +  + ]:           3 :   if (!S_ISSOCK(st.st_mode)) {
      82                 :           2 :     return "existing path is not a socket file";
      83                 :             :   }
      84                 :             : 
      85                 :           2 :   int probe_fd = ::socket(AF_UNIX, SOCK_STREAM, 0);
      86         [ -  + ]:           2 :   if (probe_fd < 0) {
      87                 :           0 :     return {};  // can't probe - fall back to prior (remove-and-proceed) behavior
      88                 :             :   }
      89                 :           2 :   struct sockaddr_un addr {};
      90                 :           2 :   addr.sun_family = AF_UNIX;
      91                 :           2 :   std::strncpy(addr.sun_path, path.c_str(), sizeof(addr.sun_path) - 1);
      92                 :           2 :   int rc = ::connect(probe_fd, reinterpret_cast<struct sockaddr*>(&addr), sizeof(addr));
      93                 :           2 :   ::close(probe_fd);
      94         [ +  + ]:           2 :   if (rc == 0) {
      95                 :           2 :     return "another process is already listening on this socket path";
      96                 :             :   }
      97                 :           1 :   return {};  // stale socket (nothing accepted the probe connect) - safe to remove
      98                 :             : #endif
      99                 :             : }
     100                 :             : 
     101                 :             : }  // namespace
     102                 :             : 
     103                 :             : struct UdsServer::Impl {
     104                 :             :   std::unique_ptr<net::io_context> owned_ioc_;
     105                 :             :   net::io_context* ioc_ = nullptr;
     106                 :             :   net::strand<net::io_context::executor_type> strand_;
     107                 :             :   std::atomic<uint64_t> generation_{0};
     108                 :             :   bool run_dispatched_ = false;
     109                 :             :   std::atomic<bool> cleanup_started_{false};
     110                 :             :   std::unique_ptr<net::executor_work_guard<net::io_context::executor_type>> work_guard_;
     111                 :             :   std::jthread ioc_thread_;
     112                 :             :   bool owns_ioc_ = true;
     113                 :             : 
     114                 :             :   std::atomic<bool> stopping_{false};
     115                 :             :   std::atomic<ClientId> next_client_id_{0};
     116                 :             :   std::mutex target_admission_mtx_;
     117                 :             :   std::mutex stop_mtx_;
     118                 :             :   std::condition_variable stop_cv_;
     119                 :             :   bool cleanup_done_ = false;
     120                 :             :   std::mutex join_mtx_;
     121                 :             : 
     122                 :         198 :   void mark_cleanup_done() {
     123                 :         198 :     detail::stop_test_hook(this, true);
     124                 :             :     {
     125                 :         198 :       std::lock_guard<std::mutex> lock(stop_mtx_);
     126                 :         198 :       cleanup_done_ = true;
     127                 :         198 :     }
     128                 :         198 :     stop_cv_.notify_all();
     129                 :         198 :   }
     130                 :             : 
     131                 :             :   // Waits for the teardown that was dispatched onto the context, and nothing
     132                 :             :   // more: the contract's precondition is that the context's owner keeps it
     133                 :             :   // running, and running it here would execute unrelated handlers - another
     134                 :             :   // channel's user callbacks included - on the stopping thread.
     135                 :         625 :   void wait_for_cleanup() {
     136                 :         625 :     detail::stop_test_hook(this, false);
     137                 :         625 :     std::unique_lock<std::mutex> lock(stop_mtx_);
     138                 :        1420 :     stop_cv_.wait(lock, [this] { return cleanup_done_; });
     139                 :         625 :   }
     140                 :             : 
     141                 :             :   // #438: only this instance's own successful bind() may remove the socket
     142                 :             :   // file on cleanup. Without this, a second UdsServer pointed at the same
     143                 :             :   // path whose start() failed before ever binding (e.g. because a live
     144                 :             :   // listener already owns that path) would still delete the first server's
     145                 :             :   // socket file the moment stop()/the destructor ran - the exact hijack
     146                 :             :   // #438 is about, just reached via stop() instead of start().
     147                 :             :   std::atomic<bool> bound_{false};
     148                 :             : 
     149                 :             :   std::unique_ptr<interface::UdsAcceptorInterface> acceptor_;
     150                 :             :   config::UdsServerConfig cfg_;
     151                 :             : 
     152                 :         191 :   concurrency::AtomicLinkState state_{base::LinkState::Idle};
     153                 :             :   // Shared snapshots for the handlers the io thread copies out per received
     154                 :             :   // chunk - a std::function copy allocates whenever the target outgrows its
     155                 :             :   // small-object buffer. See interface::SharedCallback. The connect/disconnect
     156                 :             :   // handlers stay plain: they fire once per connection, not per chunk.
     157                 :             :   interface::SharedCallback<OnBytes> on_bytes_;
     158                 :             :   interface::SharedCallback<OnState> on_state_;
     159                 :             :   interface::SharedCallback<OnBackpressure> on_bp_;
     160                 :             :   MultiClientConnectHandler on_multi_connect_;
     161                 :             :   interface::SharedCallback<MultiClientDataHandler> on_multi_data_;
     162                 :             :   MultiClientDisconnectHandler on_multi_disconnect_;
     163                 :             :   diagnostics::RuntimeStatsCounters stats_;
     164                 :             : 
     165                 :             :   mutable std::mutex sessions_mutex_;
     166                 :             :   std::unordered_map<ClientId, std::shared_ptr<UdsServerSession>> sessions_;
     167                 :             :   // Stop removes targets immediately, but their counters remain visible until
     168                 :             :   // asynchronous cleanup transfers the final snapshot exactly once.
     169                 :             :   std::unordered_map<ClientId, std::shared_ptr<UdsServerSession>> retiring_sessions_;
     170                 :             :   wrapper::SendAccounting closed_send_accounting_;
     171                 :             : 
     172                 :         221 :   void absorb_session(const std::shared_ptr<UdsServerSession>& session) {
     173                 :         221 :     const auto snapshot = session->stats();
     174                 :         221 :     stats_.absorb(snapshot);
     175                 :         221 :     diagnostics::accumulate_send_accounting(closed_send_accounting_, *snapshot.send_accounting);
     176                 :         221 :   }
     177                 :             : 
     178                 :             :   ErrorInfoHolder error_info_holder_{"uds_server"};
     179                 :             : 
     180                 :         191 :   Impl(const config::UdsServerConfig& cfg, net::io_context* ioc_ptr)
     181         [ +  + ]:         191 :       : owned_ioc_(ioc_ptr ? nullptr : std::make_unique<net::io_context>()),
     182         [ +  + ]:         191 :         ioc_(ioc_ptr ? ioc_ptr : owned_ioc_.get()),
     183                 :         191 :         strand_(net::make_strand(*ioc_)),
     184                 :         191 :         owns_ioc_(!ioc_ptr),
     185                 :         382 :         cfg_(cfg) {
     186                 :         191 :     config::detail::validate(cfg_);
     187                 :         187 :     acceptor_ = std::make_unique<BoostUdsAcceptor>(*ioc_);
     188                 :         251 :   }
     189                 :         187 :   ~Impl() {
     190                 :         187 :     stopping_ = true;
     191         [ -  + ]:         187 :     if (work_guard_) {
     192                 :           0 :       work_guard_.reset();
     193                 :             :     }
     194   [ +  -  +  + ]:         187 :     if (ioc_ && owns_ioc_) {
     195         [ -  + ]:          70 :       if (ioc_thread_.joinable()) {
     196         [ #  # ]:           0 :         if (std::this_thread::get_id() == ioc_thread_.get_id()) {
     197                 :           0 :           ioc_thread_.detach();
     198                 :             :         } else {
     199                 :           0 :           ioc_thread_.request_stop();
     200                 :           0 :           ioc_thread_.join();
     201                 :             :         }
     202                 :             :       }
     203                 :             :     }
     204                 :             :     // UDS Cleanup: socket file should be removed, but only if this instance
     205                 :             :     // actually bound it (#438).
     206         [ -  + ]:         187 :     if (bound_.load()) {
     207                 :           0 :       std::remove(cfg_.socket_path.c_str());
     208                 :             :     }
     209                 :         187 :   }
     210                 :             :   void do_accept(std::shared_ptr<UdsServer> self, uint64_t generation);
     211                 :             :   void notify_state();
     212                 :             : 
     213                 :         198 :   void finish_cleanup() {
     214         [ +  + ]:         198 :     if (bound_.exchange(false)) std::remove(cfg_.socket_path.c_str());
     215                 :         198 :     state_.set(base::LinkState::Idle);
     216         [ +  + ]:         198 :     if (owns_ioc_) work_guard_.reset();
     217                 :         198 :     mark_cleanup_done();
     218                 :         198 :   }
     219                 :             : 
     220                 :         198 :   void perform_cleanup(std::shared_ptr<UdsServer> self = {}) {
     221         [ -  + ]:         308 :     if (cleanup_started_.exchange(true)) return;
     222                 :         198 :     boost::system::error_code ec;
     223   [ +  -  +  - ]:         198 :     if (acceptor_) acceptor_->close(ec);
     224                 :         198 :     std::vector<std::pair<ClientId, std::shared_ptr<UdsServerSession>>> sessions;
     225                 :             :     {
     226                 :         198 :       std::lock_guard<std::mutex> lock(sessions_mutex_);
     227                 :         198 :       retiring_sessions_.swap(sessions_);
     228   [ +  -  +  + ]:         313 :       for (const auto& entry : retiring_sessions_) sessions.push_back(entry);
     229                 :         198 :     }
     230   [ +  +  +  - ]:         198 :     if (auto hook = detail::g_server_sessions_retiring_hook.load()) hook();
     231         [ +  + ]:         198 :     if (sessions.empty()) {
     232                 :         110 :       finish_cleanup();
     233                 :         110 :       return;
     234                 :             :     }
     235                 :             :     // Completion includes every session's callback body and cleanup. Each
     236                 :             :     // session owns its outstanding I/O; final state changes are serialized
     237                 :             :     // with accept/retry handlers on the server's management strand.
     238                 :          88 :     auto remaining = std::make_shared<size_t>(sessions.size());
     239         [ +  + ]:         203 :     for (auto& [client_id, session] : sessions) {
     240                 :         115 :       session->async_stop([this, self, remaining, client_id] {
     241                 :         115 :         net::post(strand_, [this, self, remaining, client_id] {
     242                 :             :           {
     243                 :         115 :             std::lock_guard<std::mutex> lock(sessions_mutex_);
     244                 :         115 :             const auto it = retiring_sessions_.find(client_id);
     245         [ +  - ]:         115 :             if (it != retiring_sessions_.end()) {
     246                 :         115 :               absorb_session(it->second);
     247                 :         115 :               retiring_sessions_.erase(it);
     248                 :             :             }
     249                 :         115 :           }
     250         [ +  + ]:         115 :           if (--*remaining == 0) finish_cleanup();
     251                 :         115 :         });
     252                 :         115 :       });
     253                 :             :     }
     254                 :         198 :   }
     255                 :             : 
     256                 :         659 :   void stop(std::shared_ptr<UdsServer> self) {
     257                 :         659 :     const bool on_executor = ioc_->get_executor().running_in_this_thread();
     258                 :             :     bool first;
     259                 :             :     {
     260                 :         659 :       std::lock_guard<std::mutex> lock(target_admission_mtx_);
     261                 :         659 :       first = !stopping_.exchange(true);
     262         [ +  + ]:         659 :       if (first) {
     263                 :         198 :         std::lock_guard<std::mutex> sessions_lock(sessions_mutex_);
     264         [ +  + ]:         312 :         for (auto& entry : sessions_)
     265   [ +  -  +  - ]:         114 :           if (entry.second) entry.second->request_stop();
     266                 :         198 :       }
     267                 :         659 :     }
     268         [ +  + ]:         659 :     if (first) {
     269                 :             :       {
     270                 :         198 :         std::lock_guard<std::mutex> lock(sessions_mutex_);
     271                 :         198 :         on_bytes_ = nullptr;
     272                 :         198 :         on_state_ = nullptr;
     273                 :         198 :         on_bp_ = nullptr;
     274                 :         198 :         on_multi_connect_ = nullptr;
     275                 :         198 :         on_multi_data_ = nullptr;
     276                 :         198 :         on_multi_disconnect_ = nullptr;
     277                 :         198 :       }
     278         [ +  + ]:         198 :       if (!run_dispatched_) {
     279                 :           9 :         perform_cleanup(self);
     280                 :             :       } else {
     281                 :         378 :         net::post(strand_, [this, self] { perform_cleanup(self); });
     282                 :             :       }
     283                 :             :     }
     284         [ +  + ]:         659 :     if (on_executor) return;
     285                 :         625 :     wait_for_cleanup();
     286                 :         625 :     std::lock_guard<std::mutex> join_lock(join_mtx_);
     287                 :         625 :     join_owned_thread();
     288                 :         625 :   }
     289                 :             : 
     290                 :         625 :   void join_owned_thread() {
     291   [ +  +  +  +  :         625 :     if (owns_ioc_ && ioc_thread_.joinable()) ioc_thread_.join();
                   +  + ]
     292                 :         625 :   }
     293                 :             : };
     294                 :             : 
     295                 :          73 : std::shared_ptr<UdsServer> UdsServer::create(const config::UdsServerConfig& cfg) {
     296                 :          73 :   return std::shared_ptr<UdsServer>(new UdsServer(cfg));
     297                 :             : }
     298                 :             : 
     299                 :         118 : std::shared_ptr<UdsServer> UdsServer::create(const config::UdsServerConfig& cfg,
     300                 :             :                                              std::unique_ptr<interface::UdsAcceptorInterface> acceptor,
     301                 :             :                                              net::io_context& ioc) {
     302                 :         119 :   return std::shared_ptr<UdsServer>(new UdsServer(cfg, std::move(acceptor), ioc));
     303                 :             : }
     304                 :             : 
     305                 :          76 : UdsServer::UdsServer(const config::UdsServerConfig& cfg) : impl_(std::make_unique<Impl>(cfg, nullptr)) {}
     306                 :         118 : UdsServer::UdsServer(const config::UdsServerConfig& cfg, std::unique_ptr<interface::UdsAcceptorInterface> acceptor,
     307                 :         118 :                      net::io_context& ioc)
     308                 :         118 :     : impl_(std::make_unique<Impl>(cfg, &ioc)) {
     309                 :         117 :   impl_->acceptor_ = std::move(acceptor);
     310                 :         119 : }
     311                 :             : 
     312                 :         374 : UdsServer::~UdsServer() {
     313         [ +  - ]:         187 :   if (impl_) {
     314                 :         187 :     impl_->stop(nullptr);
     315                 :             :   }
     316                 :         374 : }
     317                 :             : 
     318                 :           0 : UdsServer::UdsServer(UdsServer&&) noexcept = default;
     319                 :           0 : UdsServer& UdsServer::operator=(UdsServer&&) noexcept = default;
     320                 :             : 
     321                 :         194 : void UdsServer::start() {
     322         [ -  + ]:         199 :   if (impl_->state_.get() == base::LinkState::Listening) return;
     323                 :             : 
     324                 :         194 :   const auto generation = impl_->generation_.fetch_add(1) + 1;
     325                 :         194 :   impl_->stopping_ = false;
     326                 :         194 :   impl_->run_dispatched_ = false;
     327                 :         194 :   impl_->cleanup_started_ = false;
     328                 :             :   {
     329                 :         194 :     std::lock_guard<std::mutex> lock(impl_->stop_mtx_);
     330                 :         194 :     impl_->cleanup_done_ = false;
     331                 :         194 :   }
     332                 :             :   // Restart contract (#444): stats() resets on restart. The server-level
     333                 :             :   // counters now outlive the sessions that fed them, so clearing them here is
     334                 :             :   // what keeps that promise - before absorption they were empty and a restart
     335                 :             :   // zeroed the aggregate for free.
     336                 :             :   {
     337                 :         194 :     std::lock_guard<std::mutex> lock(impl_->sessions_mutex_);
     338                 :         194 :     impl_->stats_.reset(0);
     339                 :         194 :     impl_->closed_send_accounting_ = {};
     340                 :         194 :   }
     341                 :             : 
     342   [ +  -  -  + ]:         194 :   if (!impl_->cfg_.is_valid()) {
     343                 :           0 :     WIRESTEAD_LOG_ERROR("uds_server", "start", "Invalid UDS server configuration or socket path");
     344                 :           0 :     impl_->error_info_holder_.record_error(diagnostics::ErrorLevel::ERROR, diagnostics::ErrorCategory::CONFIGURATION,
     345                 :             :                                            "start", {}, "Invalid UDS server configuration or socket path", false, 0);
     346                 :           0 :     impl_->state_.set(base::LinkState::Error);
     347                 :           0 :     impl_->notify_state();
     348                 :           0 :     return;
     349                 :             :   }
     350                 :             : 
     351                 :             :   // #438: only remove an existing path at this location if it's actually a
     352                 :             :   // stale socket - never a regular file/directory (misconfiguration), and
     353                 :             :   // never a socket another live process is still listening on (that would
     354                 :             :   // otherwise be silently hijacked instead of failing loudly).
     355                 :         194 :   std::string block_reason = existing_uds_path_blocks_bind(impl_->cfg_.socket_path);
     356         [ +  + ]:         194 :   if (!block_reason.empty()) {
     357                 :           2 :     std::string msg = fmt::format("Refusing to bind {}: {}", impl_->cfg_.socket_path, block_reason);
     358                 :           2 :     WIRESTEAD_LOG_ERROR("uds_server", "start", msg);
     359                 :           4 :     impl_->error_info_holder_.record_error(diagnostics::ErrorLevel::ERROR, diagnostics::ErrorCategory::CONNECTION,
     360                 :           2 :                                            "start", make_error_code(boost::system::errc::address_in_use), msg, false,
     361                 :             :                                            0);
     362                 :           2 :     impl_->state_.set(base::LinkState::Error);
     363                 :           2 :     impl_->notify_state();
     364                 :           2 :     return;
     365                 :           2 :   }
     366                 :         192 :   std::remove(impl_->cfg_.socket_path.c_str());
     367                 :             : 
     368                 :         192 :   boost::system::error_code ec;
     369                 :         192 :   impl_->acceptor_->open(uds(), ec);
     370         [ -  + ]:         192 :   if (ec) {
     371                 :           0 :     std::string msg = fmt::format("Failed to open acceptor: {}", ec.message());
     372                 :           0 :     WIRESTEAD_LOG_ERROR("uds_server", "start", msg);
     373                 :           0 :     impl_->error_info_holder_.record_error(diagnostics::ErrorLevel::ERROR, diagnostics::ErrorCategory::SYSTEM, "open",
     374                 :             :                                            ec, msg, false, 0);
     375                 :           0 :     impl_->state_.set(base::LinkState::Error);
     376                 :           0 :     impl_->notify_state();
     377                 :           0 :     return;
     378                 :           0 :   }
     379                 :             : 
     380                 :         192 :   uds::endpoint endpoint;
     381                 :             :   try {
     382                 :         192 :     endpoint = uds::endpoint(impl_->cfg_.socket_path);
     383                 :           0 :   } catch (const std::exception& e) {
     384                 :           0 :     std::string msg = fmt::format("Invalid UDS endpoint: {}", e.what());
     385                 :           0 :     WIRESTEAD_LOG_ERROR("uds_server", "start", msg);
     386                 :           0 :     impl_->error_info_holder_.record_error(diagnostics::ErrorLevel::ERROR, diagnostics::ErrorCategory::CONFIGURATION,
     387                 :           0 :                                            "start", make_error_code(boost::system::errc::filename_too_long), msg, false,
     388                 :             :                                            0);
     389                 :           0 :     impl_->state_.set(base::LinkState::Error);
     390                 :           0 :     impl_->notify_state();
     391                 :           0 :     return;
     392                 :           0 :   }
     393                 :             : 
     394                 :         192 :   impl_->acceptor_->bind(endpoint, ec);
     395         [ +  + ]:         192 :   if (ec) {
     396                 :           3 :     std::string msg = fmt::format("Failed to bind to {}: {}", impl_->cfg_.socket_path, ec.message());
     397                 :           3 :     WIRESTEAD_LOG_ERROR("uds_server", "start", msg);
     398                 :           3 :     impl_->error_info_holder_.record_error(diagnostics::ErrorLevel::ERROR, diagnostics::ErrorCategory::CONNECTION,
     399                 :             :                                            "bind", ec, msg, false, 0);
     400                 :           3 :     impl_->state_.set(base::LinkState::Error);
     401                 :           3 :     impl_->notify_state();
     402                 :           3 :     return;
     403                 :           3 :   }
     404                 :         189 :   impl_->bound_.store(true);
     405                 :             : 
     406                 :         189 :   impl_->acceptor_->listen(net::socket_base::max_listen_connections, ec);
     407         [ -  + ]:         189 :   if (ec) {
     408                 :           0 :     std::string msg = fmt::format("Failed to listen: {}", ec.message());
     409                 :           0 :     WIRESTEAD_LOG_ERROR("uds_server", "start", msg);
     410                 :           0 :     impl_->error_info_holder_.record_error(diagnostics::ErrorLevel::ERROR, diagnostics::ErrorCategory::CONNECTION,
     411                 :             :                                            "listen", ec, msg, false, 0);
     412                 :           0 :     impl_->state_.set(base::LinkState::Error);
     413                 :           0 :     impl_->notify_state();
     414                 :           0 :     return;
     415                 :           0 :   }
     416                 :             : 
     417                 :             :   // #438: restrict local access to the socket file if requested. Best-effort
     418                 :             :   // - a chmod failure is logged but does not fail startup, since the socket
     419                 :             :   // is already bound and listening at this point.
     420         [ +  + ]:         189 :   if (impl_->cfg_.socket_permissions != -1) {
     421                 :             : #if !defined(WIRESTEAD_PLATFORM_WINDOWS)
     422         [ -  + ]:           1 :     if (::chmod(impl_->cfg_.socket_path.c_str(), static_cast<mode_t>(impl_->cfg_.socket_permissions)) != 0) {
     423                 :           0 :       WIRESTEAD_LOG_WARNING("uds_server", "start",
     424                 :             :                             fmt::format("Failed to chmod socket {}: {}", impl_->cfg_.socket_path, strerror(errno)));
     425                 :             :     }
     426                 :             : #else
     427                 :             :     WIRESTEAD_LOG_WARNING("uds_server", "start", "socket_permissions is ignored on Windows");
     428                 :             : #endif
     429                 :             :   }
     430                 :             : 
     431                 :         189 :   impl_->state_.set(base::LinkState::Listening);
     432                 :         189 :   impl_->notify_state();
     433         [ -  + ]:         189 :   if (impl_->stopping_) return;
     434                 :             : 
     435   [ +  +  +  -  :         189 :   if (impl_->owns_ioc_ && !impl_->ioc_thread_.joinable()) {
                   +  + ]
     436   [ +  -  -  + ]:          67 :     if (impl_->ioc_->stopped()) {
     437                 :           0 :       impl_->ioc_->restart();
     438                 :             :     }
     439                 :          67 :     impl_->work_guard_ =
     440                 :         134 :         std::make_unique<net::executor_work_guard<net::io_context::executor_type>>(net::make_work_guard(*impl_->ioc_));
     441                 :         134 :     impl_->ioc_thread_ = std::jthread([impl = impl_.get()](std::stop_token st) {
     442                 :          67 :       wirestead::concurrency::run_io_thread_init();
     443                 :             :       try {
     444                 :          67 :         std::stop_callback cb(st, [impl] { impl->ioc_->stop(); });
     445                 :          67 :         impl->ioc_->run();
     446                 :          67 :       } catch (...) {
     447                 :           0 :       }
     448                 :         134 :     });
     449                 :             :   }
     450                 :             : 
     451                 :         189 :   impl_->run_dispatched_ = true;
     452                 :         378 :   net::post(impl_->strand_, [self = shared_from_this(), generation]() { self->impl_->do_accept(self, generation); });
     453                 :         194 : }
     454                 :             : 
     455                 :         472 : void UdsServer::stop() { impl_->stop(weak_from_this().lock()); }
     456                 :             : 
     457                 :           3 : bool UdsServer::is_connected() const { return impl_->state_.get() == base::LinkState::Listening; }
     458                 :           0 : bool UdsServer::is_backpressure_active() const { return false; }
     459                 :             : 
     460                 :         128 : bool UdsServer::is_backpressure_active(ClientId client_id) const {
     461                 :         128 :   std::lock_guard<std::mutex> lock(impl_->sessions_mutex_);
     462                 :         128 :   auto it = impl_->sessions_.find(client_id);
     463   [ +  -  +  -  :         128 :   if (it != impl_->sessions_.end() && it->second) {
                   +  - ]
     464                 :         128 :     return it->second->is_backpressure_active();
     465                 :             :   }
     466                 :           0 :   return false;
     467                 :         128 : }
     468                 :             : 
     469                 :         364 : std::optional<size_t> UdsServer::write_queue_limit(ClientId client_id) const {
     470                 :         364 :   std::lock_guard<std::mutex> lock(impl_->sessions_mutex_);
     471                 :         364 :   auto it = impl_->sessions_.find(client_id);
     472   [ +  +  -  +  :         364 :   if (it == impl_->sessions_.end() || !it->second) return std::nullopt;
                   +  + ]
     473                 :         298 :   return it->second->write_queue_limit();
     474                 :         364 : }
     475                 :             : 
     476                 :         514 : boost::asio::any_io_executor UdsServer::get_executor() { return impl_->strand_; }
     477                 :             : 
     478                 :        2027 : wrapper::RuntimeStats UdsServer::stats() const {
     479                 :             :   // Snapshot retained totals and live sessions under the same lock as the
     480                 :             :   // disconnect transfer; otherwise a retiring session can disappear from both.
     481                 :        2027 :   std::lock_guard<std::mutex> lock(impl_->sessions_mutex_);
     482                 :        2027 :   auto aggregate = impl_->stats_.snapshot(0, 0, false);
     483                 :        2027 :   aggregate.send_accounting = impl_->closed_send_accounting_;
     484         [ +  + ]:        6081 :   for (const auto* sessions : {&impl_->sessions_, &impl_->retiring_sessions_}) {
     485         [ +  + ]:        4247 :     for (const auto& pair : *sessions) {
     486         [ -  + ]:         193 :       if (!pair.second) continue;
     487                 :         193 :       const auto session_stats = pair.second->stats();
     488                 :         193 :       diagnostics::accumulate_send_accounting(*aggregate.send_accounting, *session_stats.send_accounting);
     489                 :         193 :       aggregate.bytes_accepted += session_stats.bytes_accepted;
     490                 :         193 :       aggregate.messages_accepted += session_stats.messages_accepted;
     491                 :         193 :       aggregate.bytes_sent += session_stats.bytes_sent;
     492                 :         193 :       aggregate.messages_sent += session_stats.messages_sent;
     493                 :         193 :       aggregate.bytes_received += session_stats.bytes_received;
     494                 :         193 :       aggregate.messages_received += session_stats.messages_received;
     495                 :         193 :       aggregate.failed_sends += session_stats.failed_sends;
     496                 :         193 :       aggregate.dropped_messages += session_stats.dropped_messages;
     497                 :         193 :       aggregate.dropped_bytes += session_stats.dropped_bytes;
     498                 :         193 :       aggregate.backpressure_events += session_stats.backpressure_events;
     499                 :             :       // Retiring sessions retain cumulative totals, not live queue gauges.
     500         [ +  + ]:         193 :       if (sessions == &impl_->sessions_) {
     501                 :         181 :         aggregate.queued_bytes += session_stats.queued_bytes;
     502                 :         181 :         aggregate.pending_bytes += session_stats.pending_bytes;
     503   [ +  -  -  + ]:         181 :         aggregate.backpressure_active = aggregate.backpressure_active || session_stats.backpressure_active;
     504                 :             :       }
     505                 :             :       // Peak, not a total: summing per-session peaks would report a depth no
     506                 :             :       // session ever reached, because the peaks need not have been simultaneous.
     507                 :         193 :       aggregate.max_queued_bytes = std::max(aggregate.max_queued_bytes, session_stats.max_queued_bytes);
     508                 :             :     }
     509                 :             :   }
     510                 :        4054 :   return aggregate;
     511                 :        2027 : }
     512                 :             : 
     513                 :           3 : void UdsServer::reset_stats() {
     514                 :           3 :   std::lock_guard<std::mutex> lock(impl_->sessions_mutex_);
     515                 :           3 :   impl_->stats_.reset(0);
     516                 :           3 :   impl_->closed_send_accounting_ = {};
     517         [ +  + ]:           9 :   for (const auto* sessions : {&impl_->sessions_, &impl_->retiring_sessions_}) {
     518         [ +  + ]:          11 :     for (const auto& entry : *sessions) {
     519   [ +  -  +  - ]:           5 :       if (entry.second) entry.second->reset_stats();
     520                 :             :     }
     521                 :             :   }
     522                 :           3 : }
     523                 :             : 
     524                 :           5 : std::optional<diagnostics::ErrorInfo> UdsServer::last_error_info() const {
     525                 :           5 :   return impl_->error_info_holder_.last_error_info();
     526                 :             : }
     527                 :             : 
     528                 :           1 : bool UdsServer::async_write_copy(memory::ConstByteSpan data) {
     529                 :           1 :   auto shared_data = std::make_shared<const std::vector<uint8_t>>(data.begin(), data.end());
     530                 :           2 :   return async_write_shared(shared_data);
     531                 :           1 : }
     532                 :             : 
     533                 :           5 : bool UdsServer::async_write_move(std::vector<uint8_t>&& data) {
     534                 :           5 :   auto shared_data = std::make_shared<std::vector<uint8_t>>(std::move(data));
     535                 :           5 :   const bool accepted = async_write_shared(shared_data);
     536                 :             :   // Rejected session admissions retain no reference. With no accepting target,
     537                 :             :   // ownership still belongs to the caller; restore it before returning false.
     538         [ +  + ]:           5 :   if (!accepted) data = std::move(*shared_data);
     539                 :           5 :   return accepted;
     540                 :           5 : }
     541                 :             : 
     542                 :           7 : bool UdsServer::async_write_shared(std::shared_ptr<const std::vector<uint8_t>> data) {
     543   [ +  +  +  -  :           7 :   if (impl_->stopping_.load() || !data || data->empty()) {
             -  +  +  + ]
     544                 :           1 :     impl_->stats_.record_failed_send();
     545                 :           1 :     return false;
     546                 :             :   }
     547                 :           6 :   std::lock_guard<std::mutex> lock(impl_->sessions_mutex_);
     548                 :           6 :   bool sent = false;
     549                 :           6 :   bool attempted = false;
     550         [ +  + ]:          10 :   for (auto& pair : impl_->sessions_) {
     551   [ +  -  +  -  :           4 :     if (pair.second && pair.second->alive() && pair.second->async_write_shared(data)) {
          +  -  +  -  +  
          +  +  -  +  +  
                   -  - ]
     552                 :           1 :       attempted = true;
     553                 :           1 :       sent = true;
     554   [ +  -  +  -  :           3 :     } else if (pair.second && pair.second->alive()) {
             +  -  +  - ]
     555                 :           3 :       attempted = true;
     556                 :             :     }
     557                 :             :   }
     558         [ +  + ]:           6 :   if (!attempted) impl_->stats_.record_failed_send();
     559                 :           6 :   return sent;
     560                 :           6 : }
     561                 :             : 
     562                 :           0 : bool UdsServer::async_try_write_copy(memory::ConstByteSpan data) {
     563                 :           0 :   auto shared_data = std::make_shared<const std::vector<uint8_t>>(data.begin(), data.end());
     564                 :           0 :   return async_try_write_shared(shared_data);
     565                 :           0 : }
     566                 :             : 
     567                 :           4 : bool UdsServer::async_try_write_move(std::vector<uint8_t>&& data) {
     568                 :           4 :   auto shared_data = std::make_shared<std::vector<uint8_t>>(std::move(data));
     569                 :           4 :   const bool accepted = async_try_write_shared(shared_data);
     570                 :             :   // Rejected session admissions retain no reference. With no accepting target,
     571                 :             :   // ownership still belongs to the caller; restore it before returning false.
     572         [ +  + ]:           4 :   if (!accepted) data = std::move(*shared_data);
     573                 :           4 :   return accepted;
     574                 :           4 : }
     575                 :             : 
     576                 :           6 : bool UdsServer::async_try_write_shared(std::shared_ptr<const std::vector<uint8_t>> data) {
     577   [ +  +  +  -  :           6 :   if (impl_->stopping_.load() || !data || data->empty()) {
             -  +  +  + ]
     578                 :           1 :     impl_->stats_.record_failed_send();
     579                 :           1 :     return false;
     580                 :             :   }
     581                 :           5 :   std::lock_guard<std::mutex> lock(impl_->sessions_mutex_);
     582                 :           5 :   bool sent = false;
     583                 :           5 :   bool attempted = false;
     584         [ +  + ]:           9 :   for (auto& pair : impl_->sessions_) {
     585   [ +  -  +  -  :           4 :     if (pair.second && pair.second->alive()) {
             +  -  +  - ]
     586                 :           4 :       attempted = true;
     587   [ +  -  +  + ]:           4 :       if (pair.second->async_try_write_shared(data)) sent = true;
     588                 :             :     }
     589                 :             :   }
     590         [ +  + ]:           5 :   if (!attempted) impl_->stats_.record_failed_send();
     591                 :           5 :   return sent;
     592                 :           5 : }
     593                 :             : 
     594                 :         389 : void UdsServer::on_bytes(OnBytes cb) {
     595                 :         389 :   auto shared = interface::share_callback(std::move(cb));
     596                 :         389 :   std::lock_guard<std::mutex> lock(impl_->sessions_mutex_);
     597                 :         389 :   impl_->on_bytes_ = std::move(shared);
     598                 :         389 : }
     599                 :             : 
     600                 :         661 : void UdsServer::on_state(OnState cb) {
     601                 :         661 :   auto shared = interface::share_callback(std::move(cb));
     602                 :         661 :   std::lock_guard<std::mutex> lock(impl_->sessions_mutex_);
     603                 :         661 :   impl_->on_state_ = std::move(shared);
     604                 :         661 : }
     605                 :             : 
     606                 :         654 : void UdsServer::on_backpressure(OnBackpressure cb) {
     607                 :         654 :   auto shared = interface::share_callback(std::move(cb));
     608                 :         654 :   std::lock_guard<std::mutex> lock(impl_->sessions_mutex_);
     609                 :         654 :   impl_->on_bp_ = std::move(shared);
     610                 :         654 : }
     611                 :             : 
     612                 :           1 : bool UdsServer::broadcast(std::string_view message) {
     613                 :             :   auto data =
     614                 :           0 :       std::make_shared<const std::vector<uint8_t>>(reinterpret_cast<const uint8_t*>(message.data()),
     615                 :           1 :                                                    reinterpret_cast<const uint8_t*>(message.data()) + message.size());
     616                 :           2 :   return async_try_write_shared(data);
     617                 :           1 : }
     618                 :             : 
     619                 :           1 : bool UdsServer::broadcast(memory::ConstByteSpan data) {
     620                 :           1 :   auto shared_data = std::make_shared<const std::vector<uint8_t>>(data.begin(), data.end());
     621                 :           2 :   return async_try_write_shared(shared_data);
     622                 :           1 : }
     623                 :             : 
     624                 :         116 : wrapper::FanoutResult UdsServer::broadcast_result(memory::ConstByteSpan data, wrapper::SendResult wrapper_state,
     625                 :             :                                                   bool append_newline) {
     626                 :         116 :   std::vector<std::pair<ClientId, std::shared_ptr<UdsServerSession>>> targets;
     627                 :             :   {
     628                 :         116 :     std::lock_guard<std::mutex> lock(impl_->sessions_mutex_);
     629                 :         116 :     targets.reserve(impl_->sessions_.size());
     630         [ +  + ]:         307 :     for (const auto& [id, session] : impl_->sessions_) {
     631   [ +  -  +  -  :         191 :       if (session && session->alive()) targets.emplace_back(id, session);
          +  -  +  -  +  
                      - ]
     632                 :             :     }
     633                 :         116 :   }
     634   [ +  +  +  - ]:         116 :   if (auto hook = detail::g_uds_fanout_snapshot_hook.load()) hook();
     635                 :         116 :   wrapper::FanoutResult result;
     636                 :         116 :   std::shared_ptr<const std::vector<uint8_t>> shared_data;
     637         [ +  + ]:         307 :   for (const auto& [id, session] : targets) {
     638                 :             :     const auto size =
     639                 :             :         append_newline
     640   [ +  +  +  - ]:         191 :             ? (data.size() >= base::constants::MAX_BUFFER_SIZE ? base::constants::MAX_BUFFER_SIZE + 1 : data.size() + 1)
     641                 :         165 :             : data.size();
     642                 :         191 :     const auto validation = wrapper::detail::validate_payload_size(size, session->write_queue_limit());
     643         [ +  + ]:         191 :     if (!validation.accepted()) {
     644                 :          80 :       result.add(validation);
     645                 :          80 :       continue;
     646                 :             :     }
     647         [ -  + ]:         111 :     if (!wrapper_state.accepted()) {
     648                 :           0 :       result.add(wrapper_state);
     649                 :           0 :       continue;
     650                 :             :     }
     651         [ +  + ]:         111 :     if (!shared_data) {
     652                 :          63 :       auto payload = std::make_shared<std::vector<uint8_t>>();
     653   [ +  -  +  - ]:          63 :       if (!data.empty()) payload->assign(data.begin(), data.end());
     654   [ +  +  +  - ]:          63 :       if (append_newline) payload->push_back('\n');
     655                 :          63 :       shared_data = std::move(payload);
     656                 :          63 :     }
     657                 :         111 :     std::lock_guard<std::mutex> admission_lock(impl_->target_admission_mtx_);
     658                 :         111 :     const auto state = target_state();
     659         [ -  + ]:         111 :     if (!state.accepted()) {
     660                 :           0 :       result.add(state);
     661                 :           0 :       continue;
     662                 :             :     }
     663                 :         111 :     std::lock_guard<std::mutex> lock(impl_->sessions_mutex_);
     664                 :         111 :     const auto it = impl_->sessions_.find(id);
     665   [ +  -  -  +  :         111 :     if (it == impl_->sessions_.end() || it->second != session) {
                   -  + ]
     666                 :           0 :       result.add(wrapper::SendResult::reject(wrapper::SendRejection::NotReady));
     667                 :           0 :       continue;
     668                 :             :     }
     669                 :         111 :     result.add(session->try_write_shared(shared_data));
     670                 :         111 :   }
     671                 :         232 :   return result;
     672                 :         116 : }
     673                 :             : 
     674                 :          64 : bool UdsServer::send_to_client(ClientId client_id, std::string_view message) {
     675                 :         192 :   return send_to_client(client_id,
     676                 :         192 :                         memory::ConstByteSpan(reinterpret_cast<const uint8_t*>(message.data()), message.size()));
     677                 :             : }
     678                 :             : 
     679                 :          71 : bool UdsServer::send_to_client(ClientId client_id, memory::ConstByteSpan data) {
     680                 :          71 :   const auto result = write_target(client_id, data, false);
     681   [ +  +  +  - ]:          71 :   if (auto hook = detail::g_uds_server_write_result_hook.load()) hook(result);
     682                 :         142 :   return result.accepted();
     683                 :             : }
     684                 :             : 
     685                 :          16 : bool UdsServer::try_send_to_client(ClientId client_id, std::string_view message) {
     686                 :          48 :   return try_send_to_client(client_id,
     687                 :          48 :                             memory::ConstByteSpan(reinterpret_cast<const uint8_t*>(message.data()), message.size()));
     688                 :             : }
     689                 :             : 
     690                 :          23 : bool UdsServer::try_send_to_client(ClientId client_id, memory::ConstByteSpan data) {
     691                 :          23 :   const auto result = write_target(client_id, data, true);
     692   [ +  -  +  - ]:          23 :   if (auto hook = detail::g_uds_server_write_result_hook.load()) hook(result);
     693                 :          46 :   return result.accepted();
     694                 :             : }
     695                 :             : 
     696                 :        1023 : wrapper::SendResult UdsServer::target_state() const {
     697         [ +  + ]:        1023 :   if (impl_->stopping_.load()) {
     698                 :          34 :     std::lock_guard<std::mutex> lock(impl_->stop_mtx_);
     699         [ +  + ]:          34 :     return wrapper::SendResult::reject(impl_->cleanup_done_ ? wrapper::SendRejection::NotStarted
     700                 :          34 :                                                             : wrapper::SendRejection::Stopping);
     701                 :          34 :   }
     702         [ +  + ]:        1978 :   if (impl_->generation_.load() == 0) return wrapper::SendResult::reject(wrapper::SendRejection::NotStarted);
     703                 :         985 :   return wrapper::SendResult::accept();
     704                 :             : }
     705                 :             : 
     706                 :           3 : void UdsServer::fail_receive(ClientId id) {
     707                 :           3 :   auto session = capture_target(id);
     708         [ -  + ]:           3 :   if (!session) return;
     709                 :           6 :   net::dispatch(session->strand_, [session] { session->do_close(); });
     710                 :           3 : }
     711                 :             : 
     712                 :         204 : std::optional<boost::asio::any_io_executor> UdsServer::client_executor(ClientId client_id) const {
     713                 :         204 :   auto session = capture_target(client_id);
     714         [ -  + ]:         204 :   if (!session) return std::nullopt;
     715                 :         204 :   return session->strand_;
     716                 :         204 : }
     717                 :             : 
     718                 :         417 : std::shared_ptr<UdsServerSession> UdsServer::capture_target(ClientId client_id) const {
     719                 :         417 :   std::lock_guard<std::mutex> admission_lock(impl_->target_admission_mtx_);
     720   [ +  -  -  + ]:         417 :   if (!target_state().accepted()) return {};
     721                 :         417 :   std::lock_guard<std::mutex> lock(impl_->sessions_mutex_);
     722                 :         417 :   auto it = impl_->sessions_.find(client_id);
     723         [ +  + ]:         417 :   return it == impl_->sessions_.end() ? nullptr : it->second;
     724                 :         417 : }
     725                 :             : 
     726                 :         296 : std::optional<wrapper::SendResult> UdsServer::poll_target_wait(const std::shared_ptr<UdsServerSession>& session) const {
     727         [ -  + ]:         296 :   if (!session) return wrapper::SendResult::reject(wrapper::SendRejection::NotReady);
     728                 :         296 :   return session->poll_write_wait();
     729                 :             : }
     730                 :             : 
     731                 :         423 : void UdsServer::cancel_target_waits() {
     732                 :         423 :   std::lock_guard<std::mutex> admission_lock(impl_->target_admission_mtx_);
     733                 :         423 :   std::lock_guard<std::mutex> lock(impl_->sessions_mutex_);
     734         [ +  + ]:         531 :   for (auto& entry : impl_->sessions_)
     735   [ +  -  +  - ]:         108 :     if (entry.second) entry.second->cancel_write_wait();
     736                 :         423 : }
     737                 :             : 
     738                 :         259 : wrapper::SendResult UdsServer::write_target(ClientId client_id, memory::ConstByteSpan data, bool try_only,
     739                 :             :                                             const std::shared_ptr<UdsServerSession>& expected) {
     740         [ +  + ]:         259 :   if (expected) {
     741   [ +  +  +  - ]:         108 :     if (auto hook = detail::g_uds_server_pinned_write_hook.load()) hook();
     742                 :             :   }
     743                 :         259 :   std::lock_guard<std::mutex> admission_lock(impl_->target_admission_mtx_);
     744                 :         259 :   const auto state = target_state();
     745         [ +  + ]:         259 :   if (!state.accepted()) {
     746                 :          12 :     impl_->stats_.record_failed_send();
     747                 :          12 :     return state;
     748                 :             :   }
     749                 :         247 :   std::lock_guard<std::mutex> lock(impl_->sessions_mutex_);
     750                 :         247 :   auto it = impl_->sessions_.find(client_id);
     751   [ +  +  +  -  :         247 :   if (it == impl_->sessions_.end() || !it->second || (expected && it->second != expected)) {
          +  +  -  +  +  
                      + ]
     752                 :          22 :     impl_->stats_.record_failed_send();
     753                 :          22 :     return wrapper::SendResult::reject(wrapper::SendRejection::NotReady);
     754                 :             :   }
     755   [ +  +  +  -  :         225 :   return try_only ? it->second->try_write_copy(data) : it->second->write_copy(data);
                   +  - ]
     756                 :         259 : }
     757                 :             : 
     758                 :         537 : size_t UdsServer::client_count() const {
     759                 :         537 :   std::lock_guard<std::mutex> lock(impl_->sessions_mutex_);
     760                 :         537 :   size_t alive = 0;
     761         [ +  + ]:         835 :   for (const auto& [id, session] : impl_->sessions_) {
     762   [ +  -  +  -  :         298 :     if (session && session->alive()) ++alive;
             +  +  +  + ]
     763                 :             :   }
     764                 :         537 :   return alive;
     765                 :         537 : }
     766                 :             : 
     767                 :          90 : std::vector<ClientId> UdsServer::connected_clients() const {
     768                 :          90 :   std::lock_guard<std::mutex> lock(impl_->sessions_mutex_);
     769                 :          90 :   std::vector<ClientId> ids;
     770         [ +  + ]:         183 :   for (const auto& [id, session] : impl_->sessions_) {
     771   [ +  -  +  -  :          93 :     if (session && session->alive()) ids.push_back(id);
          +  +  +  +  +  
                      - ]
     772                 :             :   }
     773                 :         180 :   return ids;
     774                 :          90 : }
     775                 :             : 
     776                 :          40 : std::optional<wrapper::RuntimeStats> UdsServer::client_stats(ClientId client_id) const {
     777                 :          40 :   std::lock_guard<std::mutex> lock(impl_->sessions_mutex_);
     778                 :          40 :   auto it = impl_->sessions_.find(client_id);
     779   [ +  +  -  +  :          40 :   if (it == impl_->sessions_.end() || !it->second) return std::nullopt;
                   +  + ]
     780                 :          34 :   return it->second->stats();
     781                 :          40 : }
     782                 :             : 
     783                 :           1 : void UdsServer::set_client_limit(size_t max_clients) {
     784                 :           1 :   config::detail::range(max_clients, 0, base::constants::MAX_MAX_CONNECTIONS, "invalid client limit");
     785                 :           1 :   std::lock_guard<std::mutex> lock(impl_->sessions_mutex_);
     786                 :           2 :   impl_->cfg_.max_connections =
     787                 :           1 :       static_cast<int>(std::min(max_clients, static_cast<size_t>(base::constants::MAX_MAX_CONNECTIONS)));
     788                 :           1 : }
     789                 :             : 
     790                 :         267 : void UdsServer::on_multi_connect(MultiClientConnectHandler handler) {
     791                 :         267 :   std::lock_guard<std::mutex> lock(impl_->sessions_mutex_);
     792                 :         267 :   impl_->on_multi_connect_ = std::move(handler);
     793                 :         267 : }
     794                 :             : 
     795                 :         267 : void UdsServer::on_multi_data(MultiClientDataHandler handler) {
     796                 :         267 :   auto shared = interface::share_callback(std::move(handler));
     797                 :         267 :   std::lock_guard<std::mutex> lock(impl_->sessions_mutex_);
     798                 :         267 :   impl_->on_multi_data_ = std::move(shared);
     799                 :         267 : }
     800                 :             : 
     801                 :         264 : void UdsServer::on_multi_disconnect(MultiClientDisconnectHandler handler) {
     802                 :         264 :   std::lock_guard<std::mutex> lock(impl_->sessions_mutex_);
     803                 :         264 :   impl_->on_multi_disconnect_ = std::move(handler);
     804                 :         264 : }
     805                 :             : 
     806                 :          16 : base::LinkState UdsServer::state() const { return impl_->state_.get(); }
     807                 :             : 
     808                 :         411 : void UdsServer::Impl::do_accept(std::shared_ptr<UdsServer> self, uint64_t generation) {
     809   [ +  +  -  +  :         411 :   if (stopping_ || generation != generation_) return;
                   +  + ]
     810                 :         407 :   acceptor_->async_accept([self, generation](const boost::system::error_code& ec, uds::socket socket) {
     811                 :         403 :     net::dispatch(self->impl_->strand_, [self, generation, ec, socket = std::move(socket)]() mutable {
     812   [ +  +  -  +  :         403 :       if (self->impl_->stopping_ || generation != self->impl_->generation_) return;
                   +  + ]
     813                 :             : 
     814         [ +  + ]:         222 :       if (!ec) {
     815                 :             :         ClientId client_id;
     816                 :             :         {
     817                 :         221 :           std::lock_guard<std::mutex> lock(self->impl_->sessions_mutex_);
     818   [ +  +  -  + ]:         374 :           if (self->impl_->cfg_.max_connections > 0 &&
     819         [ -  + ]:         153 :               self->impl_->sessions_.size() >= static_cast<size_t>(self->impl_->cfg_.max_connections)) {
     820                 :           0 :             boost::system::error_code ignored;
     821                 :           0 :             socket.close(ignored);
     822                 :           0 :             auto* impl = self->impl_.get();
     823                 :           0 :             impl->do_accept(self, generation);
     824                 :           0 :             return;
     825                 :             :           }
     826                 :         221 :           client_id = self->impl_->next_client_id_++;
     827                 :         221 :         }
     828                 :             : 
     829                 :             :         auto session = std::make_shared<UdsServerSession>(
     830                 :         221 :             *self->impl_->ioc_, std::move(socket), self->impl_->cfg_.backpressure_threshold,
     831                 :         221 :             self->impl_->cfg_.idle_timeout_ms, self->impl_->cfg_.backpressure_strategy,
     832                 :         442 :             self->impl_->cfg_.enable_memory_pool, self->impl_->cfg_.read_buffer_size);
     833                 :             : 
     834                 :         221 :         std::weak_ptr<UdsServer> weak_self = self;
     835                 :         221 :         session->on_bytes([weak_self, client_id, generation](memory::ConstByteSpan data) {
     836                 :          82 :           auto s = weak_self.lock();
     837   [ +  -  +  +  :          82 :           if (!s || s->impl_->stopping_ || generation != s->impl_->generation_) return;
             -  +  +  + ]
     838                 :          81 :           interface::SharedCallback<MultiClientDataHandler> data_handler;
     839                 :          81 :           interface::SharedCallback<OnBytes> bytes_handler;
     840                 :             :           {
     841                 :          81 :             std::lock_guard<std::mutex> lock(s->impl_->sessions_mutex_);
     842                 :          81 :             data_handler = s->impl_->on_multi_data_;
     843                 :          81 :             bytes_handler = s->impl_->on_bytes_;
     844                 :          81 :           }
     845                 :          81 :           diagnostics::invoke_callback("uds_server", "on_multi_data", data_handler, client_id, data);
     846                 :          81 :           diagnostics::invoke_callback("uds_server", "on_bytes", bytes_handler, data);
     847                 :          82 :         });
     848                 :             : 
     849                 :             :         // Forward each session's pressure transitions through the current
     850                 :             :         // server handler, just like data. The snapshot permits replacement
     851                 :             :         // after connect and invokes user code outside sessions_mutex_.
     852                 :         221 :         session->on_backpressure([weak_self, generation](size_t queued) {
     853                 :         167 :           auto s = weak_self.lock();
     854   [ +  -  +  -  :         167 :           if (!s || s->impl_->stopping_ || generation != s->impl_->generation_) return;
             -  +  -  + ]
     855                 :         167 :           interface::SharedCallback<OnBackpressure> handler;
     856                 :             :           {
     857                 :         167 :             std::lock_guard<std::mutex> lock(s->impl_->sessions_mutex_);
     858                 :         167 :             handler = s->impl_->on_bp_;
     859                 :         167 :           }
     860                 :         167 :           diagnostics::invoke_callback("uds_server", "on_backpressure", handler, queued);
     861                 :         167 :         });
     862                 :             : 
     863                 :         221 :         session->on_close([weak_self, client_id, generation]() {
     864                 :         108 :           auto s = weak_self.lock();
     865   [ +  -  +  +  :         108 :           if (!s || s->impl_->stopping_ || generation != s->impl_->generation_) return;
             -  +  +  + ]
     866                 :         107 :           MultiClientDisconnectHandler disconnect_handler;
     867                 :             :           {
     868                 :         107 :             std::lock_guard<std::mutex> lock(s->impl_->sessions_mutex_);
     869                 :         107 :             disconnect_handler = s->impl_->on_multi_disconnect_;
     870                 :         107 :           }
     871                 :             :           // Keep the closing session visible to stop until notification returns,
     872                 :             :           // so native shutdown also waits for this session-strand callback.
     873                 :         107 :           diagnostics::invoke_callback("uds_server", "on_disconnect", disconnect_handler, client_id);
     874                 :             :           {
     875                 :         107 :             std::lock_guard<std::mutex> lock(s->impl_->sessions_mutex_);
     876   [ +  +  -  +  :         107 :             if (s->impl_->stopping_ || generation != s->impl_->generation_) return;
                   +  + ]
     877                 :         106 :             auto it = s->impl_->sessions_.find(client_id);
     878   [ +  -  +  -  :         106 :             if (it != s->impl_->sessions_.end() && it->second) s->impl_->absorb_session(it->second);
             +  -  +  - ]
     879                 :         106 :             s->impl_->sessions_.erase(client_id);
     880                 :         107 :           }
     881                 :         109 :         });
     882                 :             : 
     883                 :             :         {
     884                 :         221 :           std::lock_guard<std::mutex> lock(self->impl_->sessions_mutex_);
     885                 :         221 :           self->impl_->sessions_[client_id] = session;
     886                 :         221 :           auto connect_handler = self->impl_->on_multi_connect_;
     887                 :             :           // Admission becomes visible only after connect is queued on the
     888                 :             :           // session strand, ahead of reads, writes and pressure callbacks.
     889                 :         221 :           session->start_with_notification([self, client_id, generation, connect_handler = std::move(connect_handler)] {
     890   [ +  +  -  +  :         220 :             if (self->impl_->stopping_ || self->impl_->generation_ != generation) return;
                   +  + ]
     891                 :         219 :             diagnostics::invoke_callback("uds_server", "on_connect", connect_handler, client_id, "UDS Client");
     892                 :             :           });
     893                 :         221 :         }
     894                 :             : 
     895                 :             :         // Continue accepting
     896                 :         221 :         auto* impl = self->impl_.get();
     897                 :         221 :         impl->do_accept(self, generation);
     898                 :         221 :       } else {
     899                 :           1 :         auto* impl = self->impl_.get();
     900         [ -  + ]:           1 :         if (impl->stopping_.load()) return;
     901                 :             : 
     902                 :             :         // Log only real errors, not operation_aborted
     903         [ +  - ]:           1 :         if (ec != boost::asio::error::operation_aborted) {
     904                 :           1 :           std::string msg = fmt::format("Accept failed: {}", ec.message());
     905                 :           1 :           WIRESTEAD_LOG_ERROR("uds_server", "accept", msg);
     906                 :           2 :           impl->error_info_holder_.record_error(diagnostics::ErrorLevel::ERROR, diagnostics::ErrorCategory::CONNECTION,
     907                 :           1 :                                                 "accept", ec, msg, true, 0);
     908                 :             :           // Keep Listening while the accept operation is retried.
     909                 :           1 :         }
     910                 :             : 
     911         [ +  - ]:           1 :         if (!impl->stopping_.load()) {
     912                 :           1 :           auto timer = std::make_shared<net::steady_timer>(impl->strand_);
     913                 :           1 :           timer->expires_after(std::chrono::milliseconds(100));
     914                 :           1 :           timer->async_wait([self, timer, generation](const boost::system::error_code& ec) {
     915                 :           1 :             auto* retry_impl = self->impl_.get();
     916   [ +  -  +  -  :           1 :             if (!ec && !retry_impl->stopping_.load() && generation == retry_impl->generation_) {
             +  -  +  - ]
     917                 :           1 :               retry_impl->do_accept(self, generation);
     918                 :             :             }
     919                 :           1 :           });
     920                 :           1 :         }
     921                 :             :       }
     922                 :             :     });
     923                 :         403 :   });
     924                 :             : }
     925                 :             : 
     926                 :         194 : void UdsServer::Impl::notify_state() {
     927                 :         194 :   interface::SharedCallback<OnState> cb;
     928                 :             :   {
     929                 :         194 :     std::lock_guard<std::mutex> lock(sessions_mutex_);
     930                 :         194 :     cb = on_state_;
     931                 :         194 :   }
     932                 :         194 :   diagnostics::invoke_callback("uds_server", "on_state", cb, state_.get());
     933                 :         194 : }
     934                 :             : 
     935                 :             : }  // namespace transport
     936                 :             : }  // namespace wirestead
        

Generated by: LCOV version 2.0-1