GCC Code Coverage Report


Directory: ./
File: mqtt.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 283 354 79.9%
Functions: 29 33 87.9%
Branches: 275 570 48.2%

Line Branch Exec Source
1 /*
2 * This file is part of the Falcon Player (FPP) and is Copyright (C)
3 * 2013-2022 by the Falcon Player Developers.
4 *
5 * The Falcon Player (FPP) is free software, and is covered under
6 * multiple Open Source licenses. Please see the included 'LICENSES'
7 * file for descriptions of what files are covered by each license.
8 *
9 * This source file is covered under the LGPL v2.1 as described in the
10 * included LICENSE.LGPL file.
11 */
12
13 #include "fpp-pch.h"
14
15 #include "fpp-json.h"
16 #include "fppversion.h"
17
18 #include <atomic>
19 #include <cstring>
20 #include <fcntl.h>
21 #include <chrono>
22 #include <functional>
23 #include <future>
24 #include <list>
25 #include <map>
26 #include <memory>
27 #include <mosquitto.h>
28 #include <mutex>
29 #include <stdlib.h>
30 #include <string>
31 #include <thread>
32 #include <unistd.h>
33 #include <vector>
34
35 #include "Events.h"
36 #include "Timers.h"
37 #include "Warnings.h"
38 #include "common.h" // SetThreadName() -- needed directly for NOPCH builds
39 #include "common_mini.h" // FPPstrerror(), FPPrand() -- needed directly for NOPCH builds
40 #include "log.h"
41 #include "mqtt.h"
42 #include "settings.h"
43 #include "commands/Commands.h"
44
45 #define FALCON_TOPIC "falcon/player"
46
47 MosquittoClient* mqtt = NULL;
48
49 class MQTTCommand : public Command {
50 public:
51 36 MQTTCommand() :
52
2/4
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 36 times.
✗ Branch 3 not taken.
72 Command("MQTT") {
53
5/10
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 36 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 36 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 36 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 36 times.
✗ Branch 9 not taken.
216 args.push_back(CommandArg("topic", "string", "Topic"));
54
5/10
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 36 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 36 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 36 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 36 times.
✗ Branch 9 not taken.
216 args.push_back(CommandArg("message", "string", "Message"));
55
6/12
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 36 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 36 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 36 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 36 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 36 times.
✗ Branch 11 not taken.
216 args.push_back(CommandArg("retain", "bool", "Retain").setDefaultValue("false"));
56 36 }
57
58 ✗ virtual std::unique_ptr<Command::Result> run(const std::vector<std::string>& args) override {
59 ✗ if (args.size() < 2) {
60 ✗ return std::make_unique<Command::ErrorResult>("MQTT Requires two arguments");
61 }
62 ✗ bool retain = false;
63 ✗ if ((args.size() >= 3) &&
64 ✗ ((args[2] == "true") ||
65 ✗ (args[2] == "1"))) {
66 ✗ retain = true;
67 }
68
69 ✗ mqtt->PublishRaw(args[0], args[1], retain);
70 ✗ return std::make_unique<Command::Result>("OK");
71 }
72 };
73
74 36 void mosq_disc_callback(struct mosquitto* mosq, void* userdata, int level) {
75
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 if (userdata) {
76 36 ((MosquittoClient*)userdata)->HandleDisconnect();
77 }
78 /*
79 * Don't call reconnect here. Per documentation, loop_start() will handle
80 * reconnect automatically.
81 */
82 // mosquitto_reconnect(mosq);
83 36 }
84
85 // A broker that refuses the connection (bad credentials, not authorised, etc.)
86 // makes libmosquitto retry forever, invoking the connect callback with a
87 // non-zero result each attempt; count refusals so they can be logged without
88 // flooding at the retry rate.
89 static int mqttConnectRefusals = 0;
90 static long long mqttLastRefusalLogMS = 0;
91
92 // The network thread, running mosquitto_loop_forever(), and its signal that
93 // the loop has returned (see ~MosquittoClient()). Kept here rather than in
94 // MosquittoClient, whose layout plugins compile in (see mqtt.h).
95 static std::thread mqttLoopThread;
96 static std::future<void> mqttLoopStopped;
97
98 // libmosquitto opens its connection to the broker without close-on-exec, and
99 // has no hook to open it another way, so every shell and tool fppd ran
100 // inherited it. Mark it once it is open: here, on each connect, since a
101 // reconnect opens a new one, and straight after the first connect starts.
102 75 static void MarkBrokerSocketCloseOnExec(struct mosquitto* mosq) {
103 75 int fd = mosquitto_socket(mosq);
104
1/2
✓ Branch 0 taken 75 times.
✗ Branch 1 not taken.
75 if (fd >= 0) {
105 75 fcntl(fd, F_SETFD, FD_CLOEXEC);
106 }
107 75 }
108
109 39 void mosq_connect_callback(struct mosquitto* mosq, void* userdata, int rc) {
110 39 MarkBrokerSocketCloseOnExec(mosq);
111
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 35 times.
39 if (rc != 0) {
112 4 mqttConnectRefusals++;
113 4 long long now = GetMonotonicTimeMS();
114
3/4
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 3 times.
4 if ((mqttConnectRefusals == 1) || ((now - mqttLastRefusalLogMS) >= 60000)) {
115 1 mqttLastRefusalLogMS = now;
116 1 LogErr(VB_CONTROL, "MQTT broker refused connection: %s (%d attempts so far)\n",
117 mosquitto_connack_string(rc), mqttConnectRefusals);
118 }
119 4 return;
120 }
121
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 34 times.
35 if (mqttConnectRefusals) {
122 1 LogWarn(VB_CONTROL, "MQTT broker accepted connection after %d refused attempts\n",
123 mqttConnectRefusals);
124 1 mqttConnectRefusals = 0;
125 1 mqttLastRefusalLogMS = 0;
126 }
127
1/2
✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
35 if (userdata) {
128 35 ((MosquittoClient*)userdata)->HandleConnect();
129 }
130 }
131
132 13911 void mosq_log_callback(struct mosquitto* mosq, void* userdata, int level, const char* str) {
133 13911 mqtt->LogCallback(userdata, level, str);
134 13911 }
135
136 6212 void mosq_msg_callback(struct mosquitto* mosq, void* userdata, const struct mosquitto_message* message) {
137 6212 mqtt->MessageCallback(userdata, message);
138 6210 }
139
140 /*
141 *
142 */
143 // callbackTopics is walked on libmosquitto's thread for every message, and
144 // changed on the main loop and API threads as callbacks come and go. A file
145 // static rather than a member so MosquittoClient's layout doesn't change.
146 static std::mutex s_callbackTopicsLock;
147
148 6283 static std::list<std::string> CopyTopics(const std::list<std::string>& topics) {
149
1/2
✓ Branch 0 taken 6283 times.
✗ Branch 1 not taken.
6283 std::lock_guard<std::mutex> lock(s_callbackTopicsLock);
150
1/2
✓ Branch 0 taken 6283 times.
✗ Branch 1 not taken.
12566 return topics;
151 6283 }
152
153 36 MosquittoClient::MosquittoClient(const std::string& host, const int port,
154 36 const std::string& topicPrefix) :
155 36 m_port(port),
156 36 m_keepalive(60),
157 36 m_mosq(NULL),
158
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 m_host(host),
159 36 m_canProcessMessages(false),
160 36 m_isConnected(false),
161
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
72 m_topicPrefix(topicPrefix) {
162
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 LogDebug(VB_CONTROL, "MosquittoClient::MosquittoClient('%s', %d, '%s')\n",
163 host.c_str(), port, topicPrefix.c_str());
164
165
2/4
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 36 times.
✗ Branch 3 not taken.
106 WarningHolder::AddWarning(4, "MQTT Disconnected");
166
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 36 times.
36 if (m_topicPrefix.size()) {
167 ✗ m_topicPrefix += "/";
168 }
169
170
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 m_baseTopic = m_topicPrefix;
171
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 m_baseTopic += FALCON_TOPIC;
172
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 m_baseTopic += "/";
173
174
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 std::string hostname = getSetting("HostName");
175
2/4
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 36 times.
36 if (hostname == "") {
176 ✗ hostname = "FPP";
177 }
178
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 m_baseTopic += hostname;
179
180 36 } // End of MosquittoClient()
181
182 /*
183 *
184 */
185 144 MosquittoClient::~MosquittoClient() {
186 // Stop the network thread by disconnecting and joining it, not by
187 // cancelling it. A cancel lands at whatever cancellation point the thread
188 // has reached, which can be inside a message callback running a command,
189 // and the catch (...) that guards those callbacks swallows the forced
190 // unwind, which aborts the process on the way out. A clean disconnect
191 // means the broker drops our will, but shutdown has already published
192 // "ready" = 0 itself (Events::PrepareForShutdown), ahead of the
193 // DISCONNECT in the same queue.
194 //
195 // The callbacks go first: the disconnect this causes would otherwise run
196 // HandleDisconnect(), whose timer captures this object, and the others
197 // reach this object through the global `mqtt`.
198 //
199 // The join is bounded. With the broker unreachable the thread can be
200 // inside a blocking connect() (about two minutes when SYNs are dropped),
201 // or a message callback can be stuck in a command, and fppd's restart and
202 // shutdown waited on it. Every path from here ends in exec or _exit, so
203 // past the bound the client is left to the thread and never freed.
204 //
205 // The thread is ours, not mosquitto_loop_start()'s, so the join does not
206 // depend on libmosquitto still seeing it running: its own thread clears
207 // mosq->threaded on the way out, and a mosquitto_loop_stop() that read
208 // that first returned MOSQ_ERR_INVAL without joining, leaving
209 // mosquitto_destroy() to free the client under the exiting thread.
210
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
72 if (m_mosq) {
211 72 mosquitto_message_callback_set(m_mosq, nullptr);
212 72 mosquitto_log_callback_set(m_mosq, nullptr);
213 72 mosquitto_connect_callback_set(m_mosq, nullptr);
214 72 mosquitto_disconnect_callback_set(m_mosq, nullptr);
215 72 mosquitto_disconnect(m_mosq);
216
217
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
72 if (mqttLoopThread.joinable()) {
218
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 33 times.
72 if (mqttLoopStopped.wait_for(std::chrono::seconds(3)) != std::future_status::ready) {
219 6 LogWarn(VB_CONTROL, "MQTT network thread did not stop within 3 s; leaving it\n");
220 6 mqttLoopThread.detach();
221 6 mqtt = NULL;
222 6 return;
223 }
224 66 mqttLoopThread.join();
225 }
226 }
227
228 66 mosquitto_destroy(m_mosq);
229 66 mqtt = NULL;
230
231 66 mosquitto_lib_cleanup();
232
12/12
✓ Branch 0 taken 33 times.
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 33 times.
✓ Branch 3 taken 3 times.
✓ Branch 4 taken 33 times.
✓ Branch 5 taken 3 times.
✓ Branch 6 taken 33 times.
✓ Branch 7 taken 3 times.
✓ Branch 8 taken 33 times.
✓ Branch 9 taken 3 times.
✓ Branch 10 taken 33 times.
✓ Branch 11 taken 3 times.
174 }
233
234 36 void MosquittoClient::PrepareForShutdown() {
235
3/4
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 360 times.
✓ Branch 3 taken 36 times.
396 for (const std::string& n : CopyTopics(callbackTopics)) {
236
1/2
✓ Branch 0 taken 360 times.
✗ Branch 1 not taken.
360 RemoveCallback(n);
237 36 }
238
5/6
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 32 times.
✓ Branch 4 taken 4 times.
✓ Branch 5 taken 32 times.
36 if (m_canProcessMessages && m_isConnected) {
239 4 std::vector<std::string> subscribe_topics;
240
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 subscribe_topics.push_back(m_baseTopic + "/set/#");
241
242
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 4 times.
8 for (auto& subscribe : subscribe_topics) {
243
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 mosquitto_unsubscribe(m_mosq, NULL, subscribe.c_str());
244 }
245 4 }
246 36 m_canProcessMessages = false;
247 36 }
248
249 /*
250 *
251 */
252 36 int MosquittoClient::Init(const std::string& username, const std::string& password, const std::string& ca_file) {
253
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 mosquitto_lib_init();
254 // Use supplied Client Id (If one)
255
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 std::string clientId = getSetting("MQTTClientId");
256
2/4
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 36 times.
✗ Branch 3 not taken.
36 if (clientId == "") {
257 // If not, genereate one with random digits on end
258
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 clientId = getSetting("HostName");
259
2/4
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 36 times.
36 if (clientId == "") {
260 ✗ clientId = "FPP";
261 }
262
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 clientId.append("_");
263
2/2
✓ Branch 0 taken 144 times.
✓ Branch 1 taken 36 times.
180 for (int i = 0; i < 4; i++) {
264
1/2
✓ Branch 0 taken 144 times.
✗ Branch 1 not taken.
144 int digit = FPPrand() % 10;
265 144 char digitc = '0' + digit;
266
1/2
✓ Branch 0 taken 144 times.
✗ Branch 1 not taken.
144 clientId.append(1, digitc);
267 }
268 }
269
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 LogDebug(VB_CONTROL, "Using MQTT client id of %s \n", clientId.c_str());
270
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 m_mosq = mosquitto_new(clientId.c_str(), true, NULL);
271
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 36 times.
36 if (!m_mosq) {
272 ✗ LogErr(VB_CONTROL, "Error, unable to create new Mosquitto instance.\n");
273 ✗ return 0;
274 }
275
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 mosquitto_user_data_set(m_mosq, this);
276 // The network loop runs on a thread of our own (below), so libmosquitto
277 // has to lock as it would for mosquitto_loop_start().
278
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 mosquitto_threaded_set(m_mosq, true);
279
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 mosquitto_log_callback_set(m_mosq, mosq_log_callback);
280
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 mosquitto_disconnect_callback_set(m_mosq, mosq_disc_callback);
281
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 mosquitto_connect_callback_set(m_mosq, mosq_connect_callback);
282
283 // Back off failed reconnects exponentially (1s doubling up to 60s) rather
284 // than libmosquitto's default fixed 1s retry, so an unreachable or
285 // refusing broker isn't hammered once per second indefinitely.
286
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 mosquitto_reconnect_delay_set(m_mosq, 1, 60, true);
287
288
3/4
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 35 times.
36 if (username != "") {
289
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 mosquitto_username_pw_set(m_mosq, username.c_str(), password.c_str());
290 }
291
292
2/4
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 36 times.
36 if (ca_file != "") {
293 ✗ LogDebug(VB_CONTROL, "Using CA File: %s for MQTT\n", ca_file.c_str());
294 ✗ int rc = mosquitto_tls_set(m_mosq, ca_file.c_str(), NULL, NULL, NULL, NULL);
295 ✗ if (rc) {
296 ✗ LogErr(VB_CONTROL, "Error, unable to set MQTT_Ca_file. RC= %d\n", rc);
297 ✗ return 0;
298 }
299 } else {
300
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 LogDebug(VB_CONTROL, "No CA File specified for MQTT\n");
301 }
302
303
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 LogDebug(VB_CONTROL, "About to call MQTT Connect (%s, %d, %d)\n", m_host.c_str(), m_port, m_keepalive);
304
305 // Tell MQTT broker to make MQTT "ready" topic go to zero if crash or network loss
306
2/4
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 36 times.
✗ Branch 3 not taken.
36 static std::string last_ready_message = "0";
307
4/10
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 36 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 36 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 36 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
36 static std::string last_ready_topic = m_baseTopic + "/" + MQTT_READY_TOPIC_NAME;
308
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 int rc = mosquitto_will_set(m_mosq, last_ready_topic.c_str(), last_ready_message.size(), last_ready_message.c_str(), 1, true);
309
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 36 times.
36 if (rc != MOSQ_ERR_SUCCESS) {
310 ✗ LogErr(VB_CONTROL, "MQTT: Unable to set last will for %s. Error code: %d\n", MQTT_READY_TOPIC_NAME, rc);
311 }
312
313
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 rc = mosquitto_connect_async(m_mosq, m_host.c_str(), m_port, m_keepalive);
314
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 MarkBrokerSocketCloseOnExec(m_mosq);
315
316
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 36 times.
36 if (rc) {
317 ✗ LogErr(VB_CONTROL, "Error, unable to connect to Mosquitto Broker at %s: %d\n",
318 m_host.c_str(), rc);
319 ✗ LogErr(VB_CONTROL, "MQTT Error: %s\n", FPPstrerror(rc));
320 ✗ return 0;
321 }
322
323 // What mosquitto_loop_start() would run, on a thread the destructor can
324 // join: sleep for the keepalive (or a day without one) between packets,
325 // since publish() and disconnect() wake the loop.
326 36 struct mosquitto* m = m_mosq;
327
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 int timeout = m_keepalive ? m_keepalive * 1000 : 1000 * 86400;
328
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 auto stopped = std::make_shared<std::promise<void>>();
329
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 mqttLoopStopped = stopped->get_future();
330 try {
331 108 mqttLoopThread = std::thread([m, timeout, stopped]() {
332
2/4
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 36 times.
✗ Branch 3 not taken.
36 SetThreadName("FPP-MQTT");
333 36 mosquitto_loop_forever(m, timeout, 1);
334 33 stopped->set_value();
335
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
69 });
336 ✗ } catch (const std::system_error& e) {
337 ✗ LogErr(VB_CONTROL, "Error, unable to start Mosquitto loop: %s\n", e.what());
338 ✗ return 0;
339 ✗ }
340
341
5/12
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 36 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 36 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 36 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 36 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
38 CommandManager::INSTANCE.addCategorizedCommand(new MQTTCommand(), "Outputs", 1);
342
343 36 return 1;
344 36 }
345
346 /*
347 * Publish to an absolute topic. Note the parameter order is (retain, qos);
348 * the Publish() overloads below must pass them through in that order.
349 * This signature is used directly by plugins (e.g. fpp-HomeAssistant) and
350 * must not change.
351 */
352 3597 int MosquittoClient::PublishRaw(const std::string& topic, const std::string& msg, const bool retain, const int qos) {
353 3597 LogDebug(VB_CONTROL, "Publishing message '%s' on topic '%s'\n", msg.c_str(), topic.c_str());
354
355 int result;
356 {
357 // Scope the lock to just the mosquitto_publish() call, matching the
358 // coverage of the previous pthread_mutex_lock()/unlock() pair.
359
1/2
✓ Branch 0 taken 3597 times.
✗ Branch 1 not taken.
3597 std::lock_guard<std::mutex> lock(m_mosqLock);
360
1/2
✓ Branch 0 taken 3597 times.
✗ Branch 1 not taken.
3597 result = mosquitto_publish(m_mosq, NULL, topic.c_str(), msg.size(), msg.c_str(), qos, retain);
361 3597 }
362
363
2/2
✓ Branch 0 taken 48 times.
✓ Branch 1 taken 3549 times.
3597 if (result != 0) {
364 // MOSQ_ERR_NO_CONN (4) is expected when disconnected, don't spam logs
365
5/6
✓ Branch 0 taken 48 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 46 times.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 46 times.
✓ Branch 5 taken 2 times.
48 if (result == 4 && !m_isConnected) {
366 46 LogDebug(VB_CONTROL, "Cannot publish to MQTT, not connected to broker\n");
367 } else {
368 2 LogErr(VB_CONTROL, "Error running mosquitto_publish: %d\n", result);
369 }
370 48 return 0;
371 }
372
373 3549 return 1;
374 }
375
376 // Two-argument overloads used by Events::Publish() and by plugins.
377 //
378 // Everything published through here is retained (QoS 1). Historically the
379 // four-argument overloads below passed retain/qos to PublishRaw() in the wrong
380 // order, which had the effect of retaining every message regardless of the
381 // flag requested. Consumers such as Home Assistant depend on that: retained
382 // state topics (status, playlist/*, fppd_status, plugin light/switch state...)
383 // are what let a subscriber recover the current state on (re)connect instead
384 // of showing "unknown" until the next change. Keep retaining state topics so
385 // fixing the argument order does not change what is stored on the broker.
386 2145 bool MosquittoClient::Publish(const std::string& topic, const std::string& data) {
387 2145 return Publish(topic, data, true, 1);
388 }
389 1286 bool MosquittoClient::Publish(const std::string& topic, const int value) {
390
6/6
✓ Branch 0 taken 72 times.
✓ Branch 1 taken 1214 times.
✓ Branch 2 taken 36 times.
✓ Branch 3 taken 36 times.
✓ Branch 4 taken 36 times.
✓ Branch 5 taken 1250 times.
1286 if (topic == MQTT_READY_TOPIC_NAME && value) {
391 36 SetReady();
392 }
393 1286 return Publish(topic, value, true, 1);
394 }
395
396 /*
397 *
398 */
399 2215 int MosquittoClient::Publish(const std::string& subTopic, const std::string& msg, const bool retain, const int qos) {
400
2/4
✓ Branch 0 taken 2215 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2215 times.
✗ Branch 3 not taken.
2215 std::string topic = m_baseTopic + "/" + subTopic;
401
402
1/2
✓ Branch 0 taken 2215 times.
✗ Branch 1 not taken.
4430 return PublishRaw(topic, msg, retain, qos);
403 2215 }
404
405 /*
406 *
407 */
408 1321 int MosquittoClient::Publish(const std::string& subTopic, const int value, const bool retain, const int qos) {
409
2/4
✓ Branch 0 taken 1321 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1321 times.
✗ Branch 3 not taken.
1321 std::string topic = m_baseTopic + "/" + subTopic;
410 1321 std::string msg = std::to_string(value);
411
412
1/2
✓ Branch 0 taken 1321 times.
✗ Branch 1 not taken.
2642 return PublishRaw(topic, msg, retain, qos);
413 1321 }
414
415 /*
416 *
417 */
418 13911 void MosquittoClient::LogCallback(void* userdata, int level, const char* str) {
419
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 13911 times.
13911 switch (level) {
420 // FIXME, comment out the first 3 of these or tie to our own debug level
421 // case MOSQ_LOG_DEBUG:
422 // case MOSQ_LOG_INFO:
423 // case MOSQ_LOG_NOTICE:
424 ✗ case MOSQ_LOG_WARNING:
425 case MOSQ_LOG_ERR:
426 ✗ LogDebug(VB_CONTROL, "Mosquitto Log: %d:%s\n", level, str);
427 }
428 13911 }
429
430 7168 bool MosquittoClient::IsConnected() {
431 7168 return m_isConnected;
432 }
433
434 361 void MosquittoClient::RegisterCallback(const std::string& topic) {
435 361 LogDebug(VB_CONTROL, "MQTT: In AddCallback with %s\n", topic.c_str());
436 {
437
1/2
✓ Branch 0 taken 361 times.
✗ Branch 1 not taken.
361 std::lock_guard<std::mutex> lock(s_callbackTopicsLock);
438
1/2
✓ Branch 0 taken 361 times.
✗ Branch 1 not taken.
361 callbackTopics.push_back(topic);
439 361 }
440
2/6
✗ Branch 0 not taken.
✓ Branch 1 taken 361 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 361 times.
361 if (m_canProcessMessages && m_isConnected) {
441 ✗ if (topic.rfind("/set/", 0) != 0) {
442 // we are registered on all "/set/" already, no need to re-register
443 ✗ std::string tp = m_baseTopic + topic;
444 ✗ LogDebug(VB_CONTROL, "MQTT: Preparing to Subscribe to %s\n", tp.c_str());
445 ✗ int rc = mosquitto_subscribe(m_mosq, NULL, tp.c_str(), 0);
446 ✗ if (rc != MOSQ_ERR_SUCCESS) {
447 ✗ LogErr(VB_CONTROL, "Error, unable to subscribe to %s: %d\n", tp.c_str(), rc);
448 }
449 ✗ }
450 }
451 361 }
452 721 void MosquittoClient::RemoveCallback(const std::string& topic) {
453
6/6
✓ Branch 0 taken 361 times.
✓ Branch 1 taken 360 times.
✓ Branch 2 taken 41 times.
✓ Branch 3 taken 320 times.
✓ Branch 4 taken 41 times.
✓ Branch 5 taken 680 times.
721 if (m_canProcessMessages && m_isConnected) {
454
3/4
✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 24 times.
✓ Branch 3 taken 17 times.
41 if (topic.rfind("/set/", 0) != 0) {
455 // we are registered on all "/set/" already, no need to un-register
456
1/2
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
24 std::string tp = m_baseTopic + topic;
457
1/2
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
24 LogDebug(VB_CONTROL, "MQTT: Preparing to Unsubscribe to %s\n", tp.c_str());
458
1/2
✓ Branch 0 taken 24 times.
✗ Branch 1 not taken.
24 int rc = mosquitto_unsubscribe(m_mosq, NULL, tp.c_str());
459
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 24 times.
24 if (rc != MOSQ_ERR_SUCCESS) {
460 ✗ LogErr(VB_CONTROL, "Error, unable to Unsubscribe to %s: %d\n", tp.c_str(), rc);
461 }
462 24 }
463 }
464
1/2
✓ Branch 0 taken 721 times.
✗ Branch 1 not taken.
721 std::lock_guard<std::mutex> lock(s_callbackTopicsLock);
465 721 callbackTopics.remove(topic);
466 721 }
467 36 void MosquittoClient::SetReady() {
468 36 LogDebug(VB_CONTROL, "Mosquitto SetReady()\n");
469
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 if (!m_canProcessMessages) {
470 36 m_canProcessMessages = true;
471 36 mosquitto_message_callback_set(m_mosq, mosq_msg_callback);
472 }
473
474 // Register topics which were set since we originally connected
475 // But only if already connected!
476
2/2
✓ Branch 0 taken 34 times.
✓ Branch 1 taken 2 times.
36 if (m_isConnected) {
477 34 HandleConnect();
478 }
479 36 }
480
481 69 void MosquittoClient::HandleConnect() {
482 69 m_isConnected = true;
483
1/2
✓ Branch 0 taken 69 times.
✗ Branch 1 not taken.
69 LogWarn(VB_CONTROL, "Mosquitto Connected\n");
484
2/2
✓ Branch 0 taken 34 times.
✓ Branch 1 taken 35 times.
69 if (!m_canProcessMessages) {
485
1/2
✓ Branch 0 taken 34 times.
✗ Branch 1 not taken.
34 LogWarn(VB_CONTROL, "HandleConnect() called before can process messages. Won't Register topics yet.\n");
486 34 return;
487 }
488
489
1/2
✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
35 LogDebug(VB_CONTROL, "Mosquitto Connected.... Will Subscribe to Topics\n");
490 35 std::vector<std::string> subscribe_topics;
491
2/4
✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35 times.
✗ Branch 3 not taken.
35 subscribe_topics.push_back(m_baseTopic + "/set/#");
492
493
1/2
✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
35 std::string add_topics = getSetting("MQTTSubscribe");
494
495
2/4
✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 35 times.
35 if (add_topics != "") {
496 // Handle single topic with no semicolon
497 ✗ std::vector<std::string> subscribe_to_me;
498 ✗ if (add_topics.find(';') == std::string::npos && !add_topics.empty()) {
499 ✗ subscribe_to_me.push_back(add_topics);
500 }
501
502 // Handle multiple topics separated by semicolon
503 ✗ size_t start = 0, end = 0;
504 ✗ while ((end = add_topics.find(';', start)) != std::string::npos) {
505 ✗ std::string sub = add_topics.substr(start, end - start);
506 ✗ subscribe_to_me.push_back(sub);
507 ✗ start = end + 1;
508 ✗ }
509 // Add last topic if there is no trailing semicolon
510 ✗ if (start < add_topics.length()) {
511 ✗ subscribe_to_me.push_back(add_topics.substr(start));
512 }
513
514 // Trip topic names and add to subscribe list
515 ✗ for (auto& s : subscribe_to_me) {
516 ✗ s.erase(0, s.find_first_not_of(" \t\n\r\f\v"));
517 ✗ s.erase(s.find_last_not_of(" \t\n\r\f\v") + 1);
518 ✗ if (!s.empty()) {
519 ✗ LogDebug(VB_CONTROL, "Adding extra MQTT subscribe topic: %s\n", s.c_str());
520 ✗ subscribe_topics.push_back(s);
521 }
522 }
523 ✗ }
524
525
526
3/4
✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 351 times.
✓ Branch 3 taken 35 times.
386 for (auto& topic : CopyTopics(callbackTopics)) {
527
3/4
✓ Branch 0 taken 351 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 210 times.
✓ Branch 3 taken 141 times.
351 if (topic.rfind("/set/", 0) != 0) {
528
1/2
✓ Branch 0 taken 210 times.
✗ Branch 1 not taken.
210 std::string tp = m_baseTopic + topic;
529
1/2
✓ Branch 0 taken 210 times.
✗ Branch 1 not taken.
210 subscribe_topics.push_back(tp);
530 210 }
531 35 }
532
533
2/2
✓ Branch 0 taken 245 times.
✓ Branch 1 taken 35 times.
280 for (auto& subscribe : subscribe_topics) {
534
1/2
✓ Branch 0 taken 245 times.
✗ Branch 1 not taken.
245 LogDebug(VB_CONTROL, "MQTT: Preparing to Subscribe to %s\n", subscribe.c_str());
535
1/2
✓ Branch 0 taken 245 times.
✗ Branch 1 not taken.
245 int rc = mosquitto_subscribe(m_mosq, NULL, subscribe.c_str(), 0);
536
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 245 times.
245 if (rc != MOSQ_ERR_SUCCESS) {
537 ✗ LogErr(VB_CONTROL, "Error, unable to subscribe to %s: %d\n", subscribe.c_str(), rc);
538 }
539 }
540
541
3/6
✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 35 times.
✗ Branch 5 not taken.
105 WarningHolder::RemoveWarning(4, "MQTT Disconnected");
542
543 // Re-publish retained status messages on reconnection
544 // This ensures the broker has the current values after a network disconnection
545 // that may have triggered the last will (ready=0)
546
1/2
✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
35 LogDebug(VB_CONTROL, "MQTT: Re-publishing retained status on reconnection\n");
547
2/4
✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35 times.
✗ Branch 3 not taken.
70 Publish(MQTT_READY_TOPIC_NAME, 1, true, 1);
548
4/8
✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 35 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 35 times.
✗ Branch 7 not taken.
140 Publish("version", getFPPVersion(), true, 1);
549
4/8
✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 35 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 35 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 35 times.
✗ Branch 7 not taken.
105 Publish("branch", getFPPBranch(), true, 1);
550
551
1/2
✓ Branch 0 taken 35 times.
✗ Branch 1 not taken.
35 LogDebug(VB_CONTROL, "MQTT HandleConnect Complete\n");
552 35 }
553
554 36 void MosquittoClient::HandleDisconnect() {
555 36 long long tm = GetTimeMS();
556
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 32 times.
36 if (!m_isConnected) {
557 // This fires after every refused reconnect attempt; the refusal is
558 // already logged (rate-limited) in mosq_connect_callback and the
559 // "MQTT Disconnected" warning is still active, so stay quiet here.
560 4 LogDebug(VB_CONTROL, "Mosquitto Disconnected (was not connected) %lld\n", tm);
561 4 return;
562 }
563 32 LogWarn(VB_CONTROL, "Mosquitto Disconnected. Will try reconnect %lld\n", tm);
564 32 m_isConnected = false;
565
2/6
✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 32 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
95 Timers::INSTANCE.addTimer("MosquittoDisconnect", tm + 10000, [this, tm]() {
566 ✗ if (!m_isConnected) {
567 ✗ WarningHolder::AddWarning(4, "MQTT Disconnected");
568 }
569 ✗ });
570 }
571
572 /*
573 *
574 */
575 6212 void MosquittoClient::MessageCallback(void* obj, const struct mosquitto_message* message) {
576
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6212 times.
6212 if (!m_canProcessMessages) {
577 // FPPd is not yet in a state where incoming messages can be processed
578 199 return;
579 }
580 6212 bool match = 0;
581 6212 std::string emptyStr;
582 6212 std::string topic;
583 6212 std::string payload;
584
585
1/2
✓ Branch 0 taken 6212 times.
✗ Branch 1 not taken.
6212 LogDebug(VB_CONTROL, "Received Mosquitto message: '%s' on topic '%s'\n",
586 (char*)message->payload, message->topic);
587
588
1/2
✓ Branch 0 taken 6212 times.
✗ Branch 1 not taken.
6212 if (message->topic)
589
1/2
✓ Branch 0 taken 6212 times.
✗ Branch 1 not taken.
6212 topic = (char*)message->topic;
590
591
2/2
✓ Branch 0 taken 6192 times.
✓ Branch 1 taken 20 times.
6212 if (message->payload)
592
1/2
✓ Branch 0 taken 6192 times.
✗ Branch 1 not taken.
6192 payload = (char*)message->payload;
593
594
1/2
✓ Branch 0 taken 6212 times.
✗ Branch 1 not taken.
6212 CacheSetMessage(topic, payload);
595
596
3/4
✓ Branch 0 taken 6212 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 61488 times.
✓ Branch 3 taken 6011 times.
67499 for (auto& a : CopyTopics(callbackTopics)) {
597
1/2
✓ Branch 0 taken 61488 times.
✗ Branch 1 not taken.
61488 std::string s = m_baseTopic + a;
598
1/2
✓ Branch 0 taken 61488 times.
✗ Branch 1 not taken.
61488 LogDebug(VB_CONTROL, "Testing Callback '%s'\n", s.c_str());
599
1/2
✓ Branch 0 taken 61488 times.
✗ Branch 1 not taken.
61488 mosquitto_topic_matches_sub(s.c_str(), message->topic, &match);
600
2/2
✓ Branch 0 taken 201 times.
✓ Branch 1 taken 61287 times.
61488 if (match) {
601
1/2
✓ Branch 0 taken 201 times.
✗ Branch 1 not taken.
201 std::string tp = message->topic;
602
1/2
✓ Branch 0 taken 201 times.
✗ Branch 1 not taken.
201 tp.replace(0, m_baseTopic.size(), emptyStr);
603
1/2
✓ Branch 0 taken 199 times.
✗ Branch 1 not taken.
201 Events::InvokeCallback(a, tp, payload);
604
1/2
✓ Branch 0 taken 199 times.
✗ Branch 1 not taken.
199 LogDebug(VB_CONTROL, "Found and Completed Callback for '%s'\n", s.c_str());
605 199 return;
606 199 }
607
4/4
✓ Branch 0 taken 61287 times.
✓ Branch 1 taken 199 times.
✓ Branch 2 taken 6011 times.
✓ Branch 3 taken 199 times.
67696 }
608
609
1/2
✓ Branch 0 taken 6011 times.
✗ Branch 1 not taken.
6011 LogDebug(VB_CONTROL, "No match found for Mosquitto topic '%s'. Assuming it is an additional subscribe topic\n",
610 message->topic);
611
6/6
✓ Branch 0 taken 6011 times.
✓ Branch 1 taken 199 times.
✓ Branch 2 taken 6011 times.
✓ Branch 3 taken 199 times.
✓ Branch 4 taken 6011 times.
✓ Branch 5 taken 199 times.
6608 }
612
613 // Every message on every subscribed topic is cached, for conditions,
614 // variables and playlist branches to read. Unbounded, a broker with many
615 // topics -- or anyone who can publish under the subscribed wildcards, one
616 // topic name after another -- grew fppd until it ran out of memory. Past
617 // these, the topic updated longest ago makes room, and a payload too large
618 // to be a value anything here compares is not kept.
619 static constexpr size_t MQTT_CACHE_MAX_TOPICS = 4096;
620 static constexpr size_t MQTT_CACHE_MAX_PAYLOAD = 64 * 1024;
621
622 6212 void MosquittoClient::CacheSetMessage(std::string& topic, std::string& message) {
623
1/2
✓ Branch 0 taken 6212 times.
✗ Branch 1 not taken.
6212 std::unique_lock<std::mutex> lock(messageCacheLock);
624
625
1/2
✓ Branch 0 taken 6212 times.
✗ Branch 1 not taken.
6212 LogDebug(VB_CONTROL, "MosquittoClient::CacheSetMessage('%s', '%s')\n", topic.c_str(), message.c_str());
626
627
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 6212 times.
6212 if (message.size() > MQTT_CACHE_MAX_PAYLOAD) {
628 // not an older, smaller value standing in for it either
629 ✗ messageCache.erase(topic);
630 ✗ return;
631 }
632
1/2
✓ Branch 0 taken 6212 times.
✗ Branch 1 not taken.
6212 auto it = messageCache.find(topic);
633
6/6
✓ Branch 0 taken 6032 times.
✓ Branch 1 taken 180 times.
✓ Branch 2 taken 1905 times.
✓ Branch 3 taken 4127 times.
✓ Branch 4 taken 1905 times.
✓ Branch 5 taken 4307 times.
6212 if (it == messageCache.end() && messageCache.size() >= MQTT_CACHE_MAX_TOPICS) {
634 1905 auto oldest = messageCache.begin();
635
2/2
✓ Branch 0 taken 7802880 times.
✓ Branch 1 taken 1905 times.
7804785 for (auto e = messageCache.begin(); e != messageCache.end(); ++e) {
636
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 7802880 times.
7802880 if (e->second.lastUpdated < oldest->second.lastUpdated) {
637 ✗ oldest = e;
638 }
639 }
640
1/2
✓ Branch 0 taken 1905 times.
✗ Branch 1 not taken.
1905 messageCache.erase(oldest);
641 }
642 6212 messageCache[topic] = { message, time(nullptr) };
643
3/8
✓ Branch 0 taken 6212 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6212 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 6212 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
12424 }
644
645 ✗ bool MosquittoClient::CacheCheckMessage(std::string& topic, std::string& message) {
646 ✗ std::unique_lock<std::mutex> lock(messageCacheLock);
647
648 ✗ auto search = messageCache.find(topic);
649 ✗ if (search != messageCache.end())
650 ✗ return search->second.value == message;
651
652 ✗ return false;
653 ✗ }
654
655 477 bool MosquittoClient::GetCachedMessage(const std::string& topic, std::string& message) {
656 time_t lastUpdated;
657
1/2
✓ Branch 0 taken 477 times.
✗ Branch 1 not taken.
954 return GetCachedMessage(topic, message, lastUpdated);
658 }
659
660 477 bool MosquittoClient::GetCachedMessage(const std::string& topic, std::string& message, time_t& lastUpdated) {
661
1/2
✓ Branch 0 taken 477 times.
✗ Branch 1 not taken.
477 std::unique_lock<std::mutex> lock(messageCacheLock);
662
663
1/2
✓ Branch 0 taken 477 times.
✗ Branch 1 not taken.
477 auto search = messageCache.find(topic);
664
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 477 times.
477 if (search == messageCache.end())
665 ✗ return false;
666
667
1/2
✓ Branch 0 taken 477 times.
✗ Branch 1 not taken.
477 message = search->second.value;
668 477 lastUpdated = search->second.lastUpdated;
669 477 return true;
670 477 }
671
672 183 void MosquittoClient::dumpMessageCache(Json::Value& result, bool includeMetadata) {
673 // NOTE: This assumes that result is an Array
674
1/2
✓ Branch 0 taken 183 times.
✗ Branch 1 not taken.
183 std::unique_lock<std::mutex> lock(messageCacheLock);
675
676
2/2
✓ Branch 0 taken 13345 times.
✓ Branch 1 taken 183 times.
13528 for (const auto& pair : messageCache) {
677
2/2
✓ Branch 0 taken 477 times.
✓ Branch 1 taken 12868 times.
13345 if (includeMetadata) {
678
3/6
✓ Branch 0 taken 477 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 477 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 477 times.
✗ Branch 5 not taken.
477 result[pair.first]["value"] = pair.second.value;
679
3/6
✓ Branch 0 taken 477 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 477 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 477 times.
✗ Branch 5 not taken.
477 result[pair.first]["lastUpdated"] = (Json::Int64)pair.second.lastUpdated;
680 } else {
681
2/4
✓ Branch 0 taken 12868 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12868 times.
✗ Branch 3 not taken.
12868 result[pair.first] = pair.second.value;
682 }
683 }
684 183 }
685