• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

traintastic / traintastic / 26121453997

19 May 2026 07:52PM UTC coverage: 25.063% (-0.6%) from 25.624%
26121453997

Pull #221

github

web-flow
Merge 598936246 into 15e38bcf7
Pull Request #221: Xpressnet new messages

49 of 1129 new or added lines in 16 files covered. (4.34%)

782 existing lines in 21 files now uncovered.

8483 of 33847 relevant lines covered (25.06%)

172.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/server/src/network/server.cpp
1
/**
2
 * This file is part of Traintastic,
3
 * see <https://github.com/traintastic/traintastic>.
4
 *
5
 * Copyright (C) 2022-2026 Reinder Feenstra
6
 *
7
 * This program is free software; you can redistribute it and/or
8
 * modify it under the terms of the GNU General Public License
9
 * as published by the Free Software Foundation; either version 2
10
 * of the License, or (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 * GNU General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU General Public License
18
 * along with this program; if not, write to the Free Software
19
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
20
 */
21

22
#include "server.hpp"
23
#include <boost/beast/http/buffer_body.hpp>
24
#include <boost/beast/http/file_body.hpp>
25
#include <boost/url/url_view.hpp>
26
#include <boost/url/parse.hpp>
27
#include <span>
28
#include <traintastic/network/message.hpp>
29
#include <traintastic/utils/standardpaths.hpp>
30
#include <version.hpp>
31
#include "clientconnection.hpp"
32
#include "httpconnection.hpp"
33
#include "webthrottleconnection.hpp"
34
#include "../compat/stdformat.hpp"
35
#include "../core/eventloop.hpp"
36
#include "../log/log.hpp"
37
#include "../log/logmessageexception.hpp"
38
#include "../utils/endswith.hpp"
39
#include "../utils/inrange.hpp"
40
#include "../utils/setthreadname.hpp"
41
#include "../utils/startswith.hpp"
42
#include "../utils/stripprefix.hpp"
43

44
//#define SERVE_FROM_FS // Development option, NOT for production!
45
#ifdef SERVE_FROM_FS
46
  #include "../utils/readfile.hpp"
47

48
  static const auto www = std::filesystem::absolute(std::filesystem::path(__FILE__).parent_path() / ".." / ".." / "www");
49
#else
50
  #include <resource/www/throttle.html.hpp>
51
  #include <resource/www/css/throttle.css.hpp>
52
  #include <resource/www/js/throttle.js.hpp>
53
#endif
54
#include <resource/www/css/normalize.css.hpp>
55
#include <resource/shared/gfx/appicon.ico.hpp>
56

57
#define IS_SERVER_THREAD (std::this_thread::get_id() == m_threadId)
58

59
namespace beast = boost::beast;
60
namespace http = beast::http;
61

62
namespace
63
{
64

65
static constexpr std::string_view serverHeader{"Traintastic-server/" TRAINTASTIC_VERSION_FULL};
66
static constexpr std::string_view contentTypeTextPlain{"text/plain"};
67
static constexpr std::string_view contentTypeTextHtml{"text/html"};
68
static constexpr std::string_view contentTypeTextCss{"text/css"};
69
static constexpr std::string_view contentTypeTextJavaScript{"text/javascript"};
70
static constexpr std::string_view contentTypeImageXIcon{"image/x-icon"};
71
static constexpr std::string_view contentTypeImagePng{"image/png"};
72
static constexpr std::string_view contentTypeApplicationGzip{"application/gzip"};
73
static constexpr std::string_view contentTypeApplicationJson{"application/json"};
74
static constexpr std::string_view contentTypeApplicationXml{"application/xml"};
75

76
static constexpr std::array<std::string_view, 7> manualAllowedExtensions{{
77
  ".html",
78
  ".css",
79
  ".js",
80
  ".json",
81
  ".png",
82
  ".xml",
83
  ".xml.gz",
84
}};
85

86
std::string_view getContentType(std::string_view filename)
×
87
{
88
  if(endsWith(filename, ".html"))
×
89
  {
90
    return contentTypeTextHtml;
×
91
  }
92
  else if(endsWith(filename, ".png"))
×
93
  {
94
    return contentTypeImagePng;
×
95
  }
96
  else if(endsWith(filename, ".css"))
×
97
  {
98
    return contentTypeTextCss;
×
99
  }
100
  else if(endsWith(filename, ".js"))
×
101
  {
102
    return contentTypeTextJavaScript;
×
103
  }
104
  else if(endsWith(filename, ".json"))
×
105
  {
106
    return contentTypeApplicationJson;
×
107
  }
108
  else if(endsWith(filename, ".xml"))
×
109
  {
110
    return contentTypeApplicationXml;
×
111
  }
112
  else if(endsWith(filename, ".gz"))
×
113
  {
114
    return contentTypeApplicationGzip;
×
115
  }
UNCOV
116
  return {};
×
117
}
118

119
http::message_generator temporaryRedirect(const http::request<http::string_body>& request, std::string_view location)
×
120
{
121
    http::response<http::string_body> response{http::status::temporary_redirect, request.version()};
×
122
    response.set(http::field::server, serverHeader);
×
123
    response.set(http::field::location, location);
×
124
    response.set(http::field::content_type, contentTypeTextPlain);
×
125
    response.keep_alive(request.keep_alive());
×
126
    response.body() = "307 Temporary Redirect";
×
UNCOV
127
    response.prepare_payload();
×
128
    return response;
×
UNCOV
129
}
×
130

131
http::message_generator notFound(const http::request<http::string_body>& request)
×
132
{
133
  http::response<http::string_body> response{http::status::not_found, request.version()};
×
UNCOV
134
  response.set(http::field::server, serverHeader);
×
135
  response.set(http::field::content_type, contentTypeTextPlain);
×
136
  response.keep_alive(request.keep_alive());
×
137
  response.body() = "404 Not Found";
×
138
  response.prepare_payload();
×
139
  return response;
×
140
}
×
141

142
http::message_generator methodNotAllowed(const http::request<http::string_body>& request, std::initializer_list<http::verb> allowedMethods)
×
143
{
UNCOV
144
  std::string allow;
×
145
  for(auto method : allowedMethods)
×
146
  {
147
    allow.append(http::to_string(method)).append(" ");
×
148
  }
149
  http::response<http::string_body> response{http::status::method_not_allowed, request.version()};
×
UNCOV
150
  response.set(http::field::server, serverHeader);
×
151
  response.set(http::field::content_type, contentTypeTextPlain);
×
152
  response.set(http::field::allow, allow);
×
153
  response.keep_alive(request.keep_alive());
×
154
  response.body() = "405 Method Not Allowed";
×
155
  response.prepare_payload();
×
UNCOV
156
  return response;
×
157
}
×
158

UNCOV
159
http::message_generator binary(const http::request<http::string_body>& request, std::string_view contentType, std::span<const std::byte> body)
×
160
{
161
  if(request.method() != http::verb::get && request.method() != http::verb::head)
×
162
  {
UNCOV
163
    return methodNotAllowed(request, {http::verb::get, http::verb::head});
×
164
  }
165
  http::response<http::buffer_body> response{http::status::ok, request.version()};
×
166
  response.set(http::field::server, serverHeader);
×
167
  response.set(http::field::content_type, contentType);
×
UNCOV
168
  response.keep_alive(request.keep_alive());
×
169
  if(request.method() == http::verb::head)
×
170
  {
171
    response.content_length(body.size());
×
172
  }
173
  else
174
  {
175
    response.body().data = const_cast<std::byte*>(body.data());
×
176
    response.body().size = body.size();
×
177
  }
178
  response.body().more = false;
×
179
  response.prepare_payload();
×
UNCOV
180
  return response;
×
181
}
×
182

UNCOV
183
http::message_generator text(const http::request<http::string_body>& request, std::string_view contentType, std::string_view body)
×
184
{
185
  if(request.method() != http::verb::get && request.method() != http::verb::head)
×
186
  {
187
    return methodNotAllowed(request, {http::verb::get, http::verb::head});
×
188
  }
189
  http::response<http::string_body> response{http::status::ok, request.version()};
×
UNCOV
190
  response.set(http::field::server, serverHeader);
×
191
  response.set(http::field::content_type, contentType);
×
UNCOV
192
  response.keep_alive(request.keep_alive());
×
193
  if(request.method() == http::verb::head)
×
194
  {
UNCOV
195
    response.content_length(body.size());
×
196
  }
197
  else
198
  {
UNCOV
199
    response.body() = body;
×
200
  }
201
  response.prepare_payload();
×
UNCOV
202
  return response;
×
203
}
×
204

UNCOV
205
http::message_generator textPlain(const http::request<http::string_body>& request, std::string_view body)
×
206
{
UNCOV
207
  return text(request, contentTypeTextPlain, body);
×
208
}
209

UNCOV
210
http::message_generator textHtml(const http::request<http::string_body>& request, std::string_view body)
×
211
{
UNCOV
212
  return text(request, contentTypeTextHtml, body);
×
213
}
214

215
http::message_generator textCss(const http::request<http::string_body>& request, std::string_view body)
×
216
{
UNCOV
217
  return text(request, contentTypeTextCss, body);
×
218
}
219

220
http::message_generator textJavaScript(const http::request<http::string_body>& request, std::string_view body)
×
221
{
222
  return text(request, contentTypeTextJavaScript, body);
×
223
}
224

UNCOV
225
http::message_generator serveFileFromFileSystem(const http::request<http::string_body>& request, std::string_view target, const std::filesystem::path& root, std::span<const std::string_view> allowedExtensions)
×
226
{
UNCOV
227
  if(request.method() != http::verb::get && request.method() != http::verb::head)
×
228
  {
229
    return methodNotAllowed(request, {http::verb::get, http::verb::head});
×
230
  }
231

232
  if(const auto url = boost::urls::parse_origin_form(target))
×
233
  {
UNCOV
234
    const std::filesystem::path path = std::filesystem::weakly_canonical(root / url->path().substr(1));
×
235

236
    if(std::mismatch(path.begin(), path.end(), root.begin(), root.end()).second == root.end() && std::filesystem::exists(path))
×
237
    {
UNCOV
238
      const auto filename = path.string();
×
239

240
      if(endsWith(filename, allowedExtensions))
×
241
      {
242
        http::file_body::value_type file;
×
UNCOV
243
        boost::system::error_code ec;
×
244

UNCOV
245
        file.open(filename.c_str(), boost::beast::file_mode::scan, ec);
×
246
        if(!ec)
×
247
        {
248
          http::response<http::file_body> response{
249
            std::piecewise_construct,
250
            std::make_tuple(std::move(file)),
×
251
            std::make_tuple(http::status::ok, request.version())};
×
252

253
          response.set(http::field::server, serverHeader);
×
UNCOV
254
          response.set(http::field::content_type, getContentType(filename));
×
255
          response.content_length(file.size());
×
UNCOV
256
          response.keep_alive(request.keep_alive());
×
257

UNCOV
258
          if(request.method() == http::verb::head)
×
259
          {
260
            response.body().close(); // don’t send file contents
×
261
          }
262

263
          return response;
×
264
        }
×
265
      }
×
UNCOV
266
    }
×
267
  }
×
268

269
  return notFound(request);
×
270
}
271

272
}
273

274
Server::Server(bool localhostOnly, uint16_t port, bool discoverable)
×
UNCOV
275
  : m_ioContext{1}
×
276
  , m_acceptor{m_ioContext}
×
277
  , m_socketUDP{m_ioContext}
×
278
  , m_localhostOnly{localhostOnly}
×
UNCOV
279
  , m_manualPath{getManualPath()}
×
280
{
281
  assert(isEventLoopThread());
×
282

UNCOV
283
  boost::system::error_code ec;
×
284
  boost::asio::ip::tcp::endpoint endpoint(localhostOnly ? boost::asio::ip::address_v4::loopback() : boost::asio::ip::address_v4::any(), port);
×
285

286
  m_acceptor.open(endpoint.protocol(), ec);
×
UNCOV
287
  if(ec)
×
288
    throw LogMessageException(LogMessage::F1001_OPENING_TCP_SOCKET_FAILED_X, ec);
×
289

290
  m_acceptor.set_option(boost::asio::socket_base::reuse_address(true), ec);
×
UNCOV
291
  if(ec)
×
292
    throw LogMessageException(LogMessage::F1002_TCP_SOCKET_ADDRESS_REUSE_FAILED_X, ec);
×
293

294
  m_acceptor.bind(endpoint, ec);
×
UNCOV
295
  if(ec)
×
296
    throw LogMessageException(LogMessage::F1003_BINDING_TCP_SOCKET_FAILED_X, ec);
×
297

298
  m_acceptor.listen(5, ec);
×
UNCOV
299
  if(ec)
×
300
    throw LogMessageException(LogMessage::F1004_TCP_SOCKET_LISTEN_FAILED_X, ec);
×
301

302
  if(discoverable)
×
303
  {
304
    if(port == defaultPort)
×
305
    {
UNCOV
306
      m_socketUDP.open(boost::asio::ip::udp::v4(), ec);
×
UNCOV
307
      if(ec)
×
308
        throw LogMessageException(LogMessage::F1005_OPENING_UDP_SOCKET_FAILED_X, ec);
×
309

UNCOV
310
      m_socketUDP.set_option(boost::asio::socket_base::reuse_address(true), ec);
×
UNCOV
311
      if(ec)
×
UNCOV
312
        throw LogMessageException(LogMessage::F1006_UDP_SOCKET_ADDRESS_REUSE_FAILED_X, ec);
×
313

UNCOV
314
      m_socketUDP.bind(boost::asio::ip::udp::endpoint(boost::asio::ip::address_v4::any(), defaultPort), ec);
×
315
      if(ec)
×
UNCOV
316
        throw LogMessageException(LogMessage::F1007_BINDING_UDP_SOCKET_FAILED_X, ec);
×
317

318
      Log::log(id, LogMessage::N1005_DISCOVERY_ENABLED);
×
319
    }
320
    else
321
    {
UNCOV
322
      Log::log(id, LogMessage::W1001_DISCOVERY_DISABLED_ONLY_ALLOWED_ON_PORT_X, defaultPort);
×
323
      discoverable = false;
×
324
    }
325
  }
326
  else
327
    Log::log(id, LogMessage::N1006_DISCOVERY_DISABLED);
×
328

UNCOV
329
  Log::log(id, LogMessage::N1007_LISTENING_AT_X_X, m_acceptor.local_endpoint().address().to_string(), m_acceptor.local_endpoint().port());
×
330

UNCOV
331
  boost::asio::post(m_ioContext,
×
332
    [this, discoverable]()
×
333
    {
334
      if(discoverable)
×
335
        doReceive();
×
336

337
      doAccept();
×
UNCOV
338
    });
×
339

UNCOV
340
  m_thread = std::thread(
×
341
    [this]()
×
342
    {
343
#ifndef NDEBUG
UNCOV
344
      m_threadId = std::this_thread::get_id();
×
345
#endif
UNCOV
346
      setThreadName("server");
×
UNCOV
347
      m_ioContext.run();
×
348
    });
×
349
}
×
350

351
Server::~Server()
×
352
{
353
  assert(isEventLoopThread());
×
354

355
  if(!m_ioContext.stopped())
×
356
  {
357
    for(const auto& connection : m_connections)
×
358
    {
UNCOV
359
      connection->disconnect();
×
360
    }
361

362
    boost::asio::post(m_ioContext,
×
363
      [this]()
×
364
      {
365
        boost::system::error_code ec;
×
UNCOV
366
        if(m_acceptor.cancel(ec))
×
367
          Log::log(id, LogMessage::E1008_SOCKET_ACCEPTOR_CANCEL_FAILED_X, ec);
×
368

369
        m_acceptor.close();
×
370

UNCOV
371
        m_socketUDP.close();
×
372
      });
×
373
  }
374

UNCOV
375
  if(m_thread.joinable())
×
376
    m_thread.join();
×
377
}
×
378

379
void Server::connectionGone(const std::shared_ptr<WebSocketConnection>& connection)
×
380
{
381
  assert(isEventLoopThread());
×
382

383
  m_connections.erase(std::find(m_connections.begin(), m_connections.end(), connection));
×
UNCOV
384
}
×
385

UNCOV
386
void Server::doReceive()
×
387
{
UNCOV
388
  assert(IS_SERVER_THREAD);
×
389

390
  m_socketUDP.async_receive_from(boost::asio::buffer(m_udpBuffer), m_remoteEndpoint,
×
UNCOV
391
    [this](const boost::system::error_code& ec, std::size_t bytesReceived)
×
392
    {
393
      if(!ec)
×
394
      {
395
        if(bytesReceived == sizeof(Message::Header))
×
396
        {
397
          Message message(*reinterpret_cast<Message::Header*>(m_udpBuffer.data()));
×
398

399
          if(!m_localhostOnly || m_remoteEndpoint.address().is_loopback())
×
400
          {
401
            if(message.dataSize() == 0)
×
402
            {
UNCOV
403
              std::unique_ptr<Message> response = processMessage(message);
×
404
              if(response)
×
405
              {
406
                m_socketUDP.async_send_to(boost::asio::buffer(**response, response->size()), m_remoteEndpoint,
×
UNCOV
407
                  [this](const boost::system::error_code& /*ec*/, std::size_t /*bytesTransferred*/)
×
408
                  {
409
                    doReceive();
×
UNCOV
410
                  });
×
411
                return;
×
412
              }
413
            }
×
414
          }
415
        }
×
416
        doReceive();
×
417
      }
418
      else if(ec != boost::asio::error::operation_aborted)
×
419
      {
420
        Log::log(id, LogMessage::E1003_UDP_RECEIVE_ERROR_X, ec.message());
×
421
      }
422
    });
UNCOV
423
}
×
424

UNCOV
425
std::unique_ptr<Message> Server::processMessage(const Message& message)
×
426
{
427
  if(message.command() == Message::Command::Discover && message.isRequest())
×
428
  {
429
    std::unique_ptr<Message> response = Message::newResponse(message.command(), message.requestId());
×
UNCOV
430
    response->write(boost::asio::ip::host_name());
×
431
    response->write<uint16_t>(TRAINTASTIC_VERSION_MAJOR);
×
432
    response->write<uint16_t>(TRAINTASTIC_VERSION_MINOR);
×
UNCOV
433
    response->write<uint16_t>(TRAINTASTIC_VERSION_PATCH);
×
434
    assert(response->size() <= 1500); // must fit in a UDP packet
×
UNCOV
435
    return response;
×
436
  }
×
437

438
  return {};
×
439
}
440

UNCOV
441
void Server::doAccept()
×
442
{
UNCOV
443
  assert(IS_SERVER_THREAD);
×
444

445
  m_acceptor.async_accept(
×
UNCOV
446
    [this](boost::system::error_code ec, boost::asio::ip::tcp::socket socket)
×
447
    {
UNCOV
448
      if(!ec)
×
449
      {
450
        std::make_shared<HTTPConnection>(shared_from_this(), std::move(socket))->start();
×
451

452
        doAccept();
×
453
      }
UNCOV
454
      else if(ec != boost::asio::error::operation_aborted)
×
455
      {
UNCOV
456
        Log::log(id, LogMessage::E1004_TCP_ACCEPT_ERROR_X, ec.message());
×
457
      }
UNCOV
458
    });
×
UNCOV
459
}
×
460

UNCOV
461
http::message_generator Server::handleHTTPRequest(http::request<http::string_body>&& request)
×
462
{
UNCOV
463
  const auto target = request.target();
×
UNCOV
464
  if(target == "/")
×
465
  {
UNCOV
466
    return textHtml(request,
×
467
      "<!DOCTYPE html>"
468
      "<html>"
469
      "<head>"
470
        "<meta charset=\"utf-8\">"
471
        "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">"
472
        "<title>Traintastic v" TRAINTASTIC_VERSION_FULL "</title>"
473
      "</head>"
474
      "<body>"
475
        "<h1>Traintastic <small>v" TRAINTASTIC_VERSION_FULL "</small></h1>"
476
        "<ul>"
477
          "<li><a href=\"/manual/en/index.html\">Manual</a></li>"
478
          "<li><a href=\"/throttle\">Web throttle</a></li>"
479
        "</ul>"
480
      "</body>"
UNCOV
481
      "</html>");
×
482
  }
483
  if(target == "/favicon.ico")
×
484
  {
UNCOV
485
    return binary(request, contentTypeImageXIcon, Resource::shared::gfx::appicon_ico);
×
486
  }
UNCOV
487
  if(request.target() == "/css/normalize.css")
×
488
  {
UNCOV
489
    return textCss(request, Resource::www::css::normalize_css);
×
490
  }
UNCOV
491
  if(request.target() == "/css/throttle.css")
×
492
  {
493
#ifdef SERVE_FROM_FS
494
    const auto css = readFile(www / "css" / "throttle.css");
495
    return css ? textCss(request, *css) : notFound(request);
496
#else
UNCOV
497
    return textCss(request, Resource::www::css::throttle_css);
×
498
#endif
499
  }
UNCOV
500
  if(request.target() == "/js/throttle.js")
×
501
  {
502
#ifdef SERVE_FROM_FS
503
    const auto js = readFile(www / "js" / "throttle.js");
504
    return js ? textJavaScript(request, *js) : notFound(request);
505
#else
506
    return textJavaScript(request, Resource::www::js::throttle_js);
×
507
#endif
508
  }
UNCOV
509
  if(request.target() == "/throttle")
×
510
  {
511
#ifdef SERVE_FROM_FS
512
    const auto html = readFile(www / "throttle.html");
513
    return html ? textHtml(request, *html) : notFound(request);
514
#else
UNCOV
515
    return textHtml(request, Resource::www::throttle_html);
×
516
#endif
517
  }
UNCOV
518
  if(target == "/version")
×
519
  {
UNCOV
520
    return textPlain(request, TRAINTASTIC_VERSION_FULL);
×
521
  }
UNCOV
522
  if(startsWith(target, "/manual"))
×
523
  {
UNCOV
524
    if(target.size() == 10 && target[7] == '/' && inRange(target[8], 'a', 'z') && inRange(target[9], 'a', 'z'))
×
525
    {
UNCOV
526
      auto language = std::string_view(target).substr(8, 2);
×
527
      if(std::filesystem::exists(m_manualPath / language / "index.html"))
×
528
      {
529
        return temporaryRedirect(request, std::format("/manual/{}/index.html", language));
×
530
      }
UNCOV
531
      return temporaryRedirect(request, "/manual/en/index.html");
×
532
    }
533
    return serveFileFromFileSystem(
534
      request,
UNCOV
535
      stripPrefix(target, "/manual"),
×
UNCOV
536
      m_manualPath,
×
537
      manualAllowedExtensions);
×
538
  }
539
  return notFound(request);
×
540
}
541

542
bool Server::handleWebSocketUpgradeRequest(http::request<http::string_body>&& request, beast::tcp_stream& stream)
×
543
{
544
  if(request.target() == "/client")
×
545
  {
UNCOV
546
    return acceptWebSocketUpgradeRequest<ClientConnection>(std::move(request), stream);
×
547
  }
548
  if(request.target() == "/throttle")
×
549
  {
550
    return acceptWebSocketUpgradeRequest<WebThrottleConnection>(std::move(request), stream);
×
551
  }
552
  return false;
×
553
}
554

555
template<class T>
556
bool Server::acceptWebSocketUpgradeRequest(http::request<http::string_body>&& request, beast::tcp_stream& stream)
×
557
{
558
  namespace websocket = beast::websocket;
559

UNCOV
560
  beast::get_lowest_layer(stream).expires_never(); // disable HTTP timeout
×
561

562
  auto ws = std::make_shared<websocket::stream<beast::tcp_stream>>(std::move(stream));
×
UNCOV
563
  ws->set_option(websocket::stream_base::timeout::suggested(beast::role_type::server));
×
564
  ws->set_option(websocket::stream_base::decorator(
×
UNCOV
565
    [](websocket::response_type& response)
×
566
    {
UNCOV
567
      response.set(beast::http::field::server, serverHeader);
×
568
    }));
569

UNCOV
570
  ws->async_accept(request,
×
UNCOV
571
    [this, ws](beast::error_code ec)
×
572
    {
UNCOV
573
      if(!ec)
×
574
      {
UNCOV
575
        auto connection = std::make_shared<T>(*this, ws);
×
UNCOV
576
        connection->start();
×
577

UNCOV
578
        EventLoop::call(
×
UNCOV
579
          [this, connection]()
×
580
          {
UNCOV
581
            Log::log(connection->id, LogMessage::I1003_NEW_CONNECTION);
×
UNCOV
582
            m_connections.push_back(connection);
×
583
          });
UNCOV
584
      }
×
UNCOV
585
      else if(ec != boost::asio::error::operation_aborted)
×
586
      {
UNCOV
587
        Log::log(id, LogMessage::E1004_TCP_ACCEPT_ERROR_X, ec.message());
×
588
      }
589
    });
590

UNCOV
591
  return true;
×
UNCOV
592
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc