| 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 |