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

traintastic / traintastic / 21101426787

17 Jan 2026 09:52PM UTC coverage: 28.005% (+0.01%) from 27.995%
21101426787

push

github

reinder
[traintastic] fixed restart crash (out of memory)

12 of 38 new or added lines in 11 files covered. (31.58%)

5 existing lines in 2 files now uncovered.

8165 of 29156 relevant lines covered (28.0%)

193.78 hits per line

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

0.0
/server/src/network/webthrottleconnection.cpp
1
/**
2
 * This file is part of Traintastic,
3
 * see <https://github.com/traintastic/traintastic>.
4
 *
5
 * Copyright (C) 2025-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 "webthrottleconnection.hpp"
23
#include "server.hpp"
24
#include "../traintastic/traintastic.hpp"
25
#include "../core/errorcode.hpp"
26
#include "../core/eventloop.hpp"
27
#include "../core/method.tpp"
28
#include "../core/objectproperty.tpp"
29
#include "../hardware/decoder/decoder.hpp"
30
#include "../throttle/webthrottle.hpp"
31
#include "../log/log.hpp"
32
#include "../train/train.hpp"
33
#include "../train/trainlist.hpp"
34
#include "../train/trainvehiclelist.hpp"
35

36
WebThrottleConnection::WebThrottleConnection(Server& server, std::shared_ptr<boost::beast::websocket::stream<boost::beast::tcp_stream>> ws)
×
37
  : WebSocketConnection(server, std::move(ws), "webthrottle")
×
38
{
39
  assert(isServerThread());
×
40

41
  m_ws->binary(false);
×
42
}
×
43

44
WebThrottleConnection::~WebThrottleConnection()
×
45
{
46
  assert(isEventLoopThread());
×
47

48
  // disconnect all signals:
49
  m_traintasticPropertyChanged.disconnect();
×
50
  m_trainConnections.clear();
×
51
  m_throttleConnections.clear();
×
52

53
  // destroy all throttles:
54
  for(auto& it : m_throttles)
×
55
  {
56
    it.second->destroy();
×
57
    it.second.reset();
×
58
  }
59
}
×
60

61
void WebThrottleConnection::start()
×
62
{
63
  WebSocketConnection::start();
×
64

65
  EventLoop::call(
×
66
    [this]()
×
67
    {
68
      m_traintasticPropertyChanged = Traintastic::instance->propertyChanged.connect(
×
69
        [this](BaseProperty& property)
×
70
        {
71
          if(property.name() == "world")
×
72
          {
73
            assert(m_throttles.empty());
×
74
            sendWorld(static_cast<ObjectProperty<World>&>(property).value());
×
75
          }
76
        });
×
77

78
      sendWorld(Traintastic::instance->world.value());
×
79
    });
×
80
}
×
81

82
void WebThrottleConnection::doRead()
×
83
{
84
  assert(isServerThread());
×
85

86
  m_ws->async_read(m_readBuffer,
×
87
    [this, weak=weak_from_this()](const boost::system::error_code& ec, std::size_t /*bytesReceived*/)
×
88
    {
89
      if(weak.expired())
×
90
        return;
×
91

92
      if(!ec)
×
93
      {
94
        std::string_view sv(static_cast<const char*>(m_readBuffer.cdata().data()), m_readBuffer.size());
×
95

96
        EventLoop::call(
×
97
          [this, message=nlohmann::json::parse(sv)]()
×
98
          {
99
            processMessage(message);
×
100
          });
×
101
        m_readBuffer.consume(m_readBuffer.size());
×
102
        doRead();
×
103
      }
104
      else if(
×
105
          ec == boost::asio::error::eof ||
×
106
          ec == boost::asio::error::connection_aborted ||
×
107
          ec == boost::asio::error::connection_reset)
×
108
      {
109
        // Socket read failed (The WebSocket stream was gracefully closed at both endpoints)
110
        EventLoop::call(std::bind(&WebThrottleConnection::connectionLost, this));
×
111
      }
NEW
112
      else if(ec != boost::asio::error::operation_aborted)
×
113
      {
114
        Log::log(id, LogMessage::E1007_SOCKET_READ_FAILED_X, ec);
×
115
        EventLoop::call(std::bind(&WebThrottleConnection::disconnect, this));
×
116
      }
117
    });
118
}
×
119

120
void WebThrottleConnection::doWrite()
×
121
{
122
  assert(isServerThread());
×
123

124
  m_ws->async_write(boost::asio::buffer(m_writeQueue.front().data(), m_writeQueue.front().size()),
×
125
    [this, weak=weak_from_this()](const boost::system::error_code& ec, std::size_t /*bytesTransferred*/)
×
126
    {
127
      if(weak.expired())
×
128
        return;
×
129

130
      if(!ec)
×
131
      {
132
        m_writeQueue.pop();
×
133
        if(!m_writeQueue.empty())
×
134
          doWrite();
×
135
      }
136
      else if(ec != boost::asio::error::operation_aborted)
×
137
      {
138
        Log::log(id, LogMessage::E1006_SOCKET_WRITE_FAILED_X, ec);
×
139
        EventLoop::call(std::bind(&WebThrottleConnection::disconnect, this));
×
140
      }
141
    });
142
}
×
143

144
void WebThrottleConnection::processMessage(const nlohmann::json& message)
×
145
{
146
  assert(isEventLoopThread());
×
147

148
  const auto& world = Traintastic::instance->world.value();
×
149
  const auto action = message.value("action", "");
×
150
  const auto throttleId = message.value<uint32_t>("throttle_id", 0);
×
151

152
  if(throttleId == 0)
×
153
  {
154
    if(action == "get_train_list")
×
155
    {
156
      auto response = nlohmann::json::object();
×
157
      response.emplace("event", "train_list");
×
158
      auto list = nlohmann::json::array();
×
159
      if(world)
×
160
      {
161
        for(const auto& train : *world->trains)
×
162
        {
163
          auto item = nlohmann::json::object();
×
164
          item.emplace("id", train->id.value());
×
165
          item.emplace("name", train->name.value());
×
166
          list.emplace_back(item);
×
167
        }
×
168
      }
169
      response.emplace("list", list);
×
170
      sendMessage(response);
×
171
    }
×
172
    else if(action == "estop_all")
×
173
    {
174
      for(const auto& it : m_throttles)
×
175
      {
176
        it.second->emergencyStop();
×
177
      }
178
    }
179
  }
180
  else
181
  {
182
    const auto& throttle = getThrottle(throttleId);
×
183

184
    if(!throttle)
×
185
    {
186
      if(Traintastic::instance->world.value())
×
187
      {
188
        sendError(throttleId, "No world loaded", "no_world_loaded");
×
189
      }
190
      else
191
      {
192
        sendError(throttleId, "Failed to create throttle");
×
193
      }
194
      return;
×
195
    }
196

197
    if(action == "acquire")
×
198
    {
199
      auto train = std::dynamic_pointer_cast<Train>(world->getObjectById(message.value("train_id", "")));
×
200
      if(train)
×
201
      {
202
        nlohmann::json object;
×
203

204
        const auto ec = throttle->acquire(train, message.value("steal", false));
×
205
        if(!ec)
×
206
        {
207
          m_trainConnections.erase(throttleId);
×
208

209
          m_trainConnections.emplace(throttleId, train->propertyChanged.connect(
×
210
            [this, throttleId](BaseProperty& property)
×
211
            {
212
              const auto name = property.name();
×
213
              if(name == "direction" || name == "speed" || name == "throttle_speed" || name == "is_stopped")
×
214
              {
215
                auto event = nlohmann::json::object();
×
216
                event.emplace("event", name);
×
217
                event.emplace("throttle_id", throttleId);
×
218
                if(dynamic_cast<AbstractUnitProperty*>(&property))
×
219
                {
220
                  event.update(property.toJSON());
×
221
                }
222
                else
223
                {
224
                  event.emplace("value", property.toJSON());
×
225
                }
226
                sendMessage(event);
×
227
              }
×
228
            }));
×
229

230
          object = nlohmann::json::object();
×
231
          object.emplace("id", train->id.toJSON());
×
232
          object.emplace("name", train->name.toJSON());
×
233
          object.emplace("direction", train->direction.toJSON());
×
234
          object.emplace("is_stopped", train->isStopped.toJSON());
×
235
          object.emplace("speed", train->speed.toJSON());
×
236
          object.emplace("throttle_speed", train->throttleSpeed.toJSON());
×
237

238
          auto functions = nlohmann::json::array();
×
239
          for(const auto& vehicle : *train->vehicles)
×
240
          {
241
            if(const auto& decoder = vehicle->decoder.value(); decoder && !decoder->functions->empty())
×
242
            {
243
              auto group = nlohmann::json::object();
×
244
              group.emplace("id", vehicle->id.toJSON());
×
245
              group.emplace("name", vehicle->name.toJSON());
×
246
              auto items = nlohmann::json::array();
×
247
              for(const auto& function : *decoder->functions)
×
248
              {
249
                m_trainConnections.emplace(throttleId, function->propertyChanged.connect(
×
250
                  [this, throttleId, vehicleId=vehicle->id.value()](BaseProperty& property)
×
251
                  {
252
                    if(property.name() == "value")
×
253
                    {
254
                      const auto& decoderFunction = static_cast<const DecoderFunction&>(property.object());
×
255
                      auto event = nlohmann::json::object();
×
256
                      event.emplace("event", "function_value");
×
257
                      event.emplace("throttle_id", throttleId);
×
258
                      event.emplace("vehicle_id", vehicleId);
×
259
                      event.emplace("number", decoderFunction.number.toJSON());
×
260
                      event.emplace("value", property.toJSON());
×
261
                      sendMessage(event);
×
262
                    }
×
263
                  }));
×
264

265
                auto item = nlohmann::json::object();
×
266
                item.emplace("number", function->number.toJSON());
×
267
                item.emplace("name", function->name.toJSON());
×
268
                item.emplace("type", function->type.toJSON());
×
269
                item.emplace("function", function->function.toJSON());
×
270
                item.emplace("value", function->value.toJSON());
×
271
                items.emplace_back(item);
×
272
              }
×
273
              group.emplace("items", items);
×
274
              functions.emplace_back(group);
×
275
            }
×
276
          }
277
          object.emplace("functions", functions);
×
278

279
          auto response = nlohmann::json::object();
×
280
          response.emplace("event", "train");
×
281
          response.emplace("throttle_id", throttleId);
×
282
          response.emplace("train", object);
×
283
          sendMessage(response);
×
284
        }
×
285
        else // error
286
        {
287
          sendError(throttleId, ec);
×
288
        }
289
      }
×
290
    }
×
291
    else if(action == "set_name")
×
292
    {
293
      throttle->name = message.value("value", "");
×
294
    }
295
    else if(throttle->acquired())
×
296
    {
297
      if(action == "estop")
×
298
      {
299
        throttle->emergencyStop();
×
300
      }
301
      else if(action == "stop")
×
302
      {
303
        if(message.value("immediate", false))
×
304
        {
305
          throttle->train->setSpeed(*throttle, 0.0);
×
306
        }
307
        else
308
        {
309
          throttle->train->setTargetSpeed(*throttle, 0.0);
×
310
        }
311
      }
312
      else if(action == "faster")
×
313
      {
314
        throttle->faster(message.value("immediate", false));
×
315
      }
316
      else if(action == "slower")
×
317
      {
318
        throttle->slower(message.value("immediate", false));
×
319
      }
320
      else if(action == "reverse" || action == "forward")
×
321
      {
322
        const auto direction = (action == "forward") ? Direction::Forward : Direction::Reverse;
×
323
        if(const auto ec = throttle->train->setDirection(*throttle, direction); ec)
×
324
        {
325
          sendError(throttleId, ec);
×
326
        }
327
      }
328
      else if(action == "release")
×
329
      {
330
        throttle->release(message.value("stop", true));
×
331
        released(throttleId);
×
332
      }
333
      else if(action == "toggle_function")
×
334
      {
335
        const auto vehicleId = message.value<std::string_view>("vehicle_id", {});
×
336
        const auto functionNumber = message.value<uint32_t>("function_number", 0);
×
337

338
        for(const auto& vehicle : *throttle->train->vehicles)
×
339
        {
340
          if(vehicle->id.value() == vehicleId && vehicle->decoder)
×
341
          {
342
            if(const auto& function = vehicle->decoder->getFunction(functionNumber))
×
343
            {
344
              function->value = !function->value;
×
345
            }
346
          }
347
        }
348
      }
349
    }
350
  }
351
}
×
352

353
void WebThrottleConnection::sendMessage(const nlohmann::json& message)
×
354
{
355
  assert(isEventLoopThread());
×
356

357
  ioContext().post(
×
358
    [this, msg=message.dump()]()
×
359
    {
360
      const bool wasEmpty = m_writeQueue.empty();
×
361
      m_writeQueue.push(msg);
×
362
      if(wasEmpty)
×
363
      {
364
        doWrite();
×
365
      }
366
    });
×
367
}
×
368

369
void WebThrottleConnection::sendError(uint32_t throttleId, std::string_view text, std::string_view tag)
×
370
{
371
  assert(isEventLoopThread());
×
372

373
  auto error = nlohmann::json::object();
×
374
  error.emplace("event", "message");
×
375
  error.emplace("throttle_id", throttleId);
×
376
  error.emplace("type", "error");
×
377
  if(!tag.empty())
×
378
  {
379
    error.emplace("tag", tag);
×
380
  }
381
  error.emplace("text", text);
×
382
  sendMessage(error);
×
383
}
×
384

385
void WebThrottleConnection::sendError(uint32_t throttleId, std::error_code ec)
×
386
{
387
  assert(isEventLoopThread());
×
388

389
  if(ec == ErrorCode::AlreadyAcquired)
×
390
  {
391
    sendError(throttleId, ec.message(), "already_acquired");
×
392
  }
393
  else if(ec == ErrorCode::CanNotActivateTrain)
×
394
  {
395
    sendError(throttleId, ec.message(), "can_not_activate_train");
×
396
  }
397
  else if(ec == ErrorCode::TrainMustBeStoppedToChangeDirection)
×
398
  {
399
    sendError(throttleId, ec.message(), "train_must_be_stopped_to_change_direction");
×
400
  }
401
  else
402
  {
403
    sendError(throttleId, ec.message());
×
404
  }
405
}
×
406

407
void WebThrottleConnection::sendWorld(const std::shared_ptr<World>& world)
×
408
{
409
  assert(isEventLoopThread());
×
410

411
  auto event = nlohmann::json::object();
×
412
  event.emplace("event", "world");
×
413
  if(world)
×
414
  {
415
    event.emplace("name", world->name.toJSON());
×
416
  }
417
  else
418
  {
419
    event.emplace("name", nullptr);
×
420
  }
421
  sendMessage(event);
×
422
}
×
423

424
const std::shared_ptr<WebThrottle>& WebThrottleConnection::getThrottle(uint32_t throttleId)
×
425
{
426
  assert(isEventLoopThread());
×
427

428
  static const std::shared_ptr<WebThrottle> noThrottle;
×
429

430
  if(auto it = m_throttles.find(throttleId); it != m_throttles.end())
×
431
  {
432
    return it->second;
×
433
  }
434

435
  if(const auto& world = Traintastic::instance->world.value())
×
436
  {
437
    auto [it, inserted] = m_throttles.emplace(throttleId, WebThrottle::create(*world));
×
438
    if(inserted) /*[[likely]]*/
×
439
    {
440
      m_throttleConnections.emplace(throttleId, it->second->onDestroying.connect(
×
441
        [this, throttleId](Object& /*object*/)
×
442
        {
443
          released(throttleId);
×
444
          m_throttleConnections.erase(throttleId);
×
445
          m_throttles.erase(throttleId);
×
446
        }));
×
447
      m_throttleConnections.emplace(throttleId, it->second->onRelease.connect(
×
448
        [this, throttleId](const std::shared_ptr<Throttle>& /*throttle*/)
×
449
        {
450
          released(throttleId);
×
451
        }));
×
452
      return it->second;
×
453
    }
454
  }
455

456
  return noThrottle;
×
457
}
458

459
void WebThrottleConnection::released(uint32_t throttleId)
×
460
{
461
  assert(isEventLoopThread());
×
462

463
  auto response = nlohmann::json::object();
×
464
  response.emplace("event", "train");
×
465
  response.emplace("throttle_id", throttleId);
×
466
  response.emplace("train", nullptr);
×
467
  sendMessage(response);
×
468
}
×
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