GCC Code Coverage Report


Directory: ./
File: Events.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 146 170 85.9%
Functions: 22 27 81.5%
Branches: 134 266 50.4%

Line Branch Exec Source
1 /*
2 * This file is part of the Falcon Player (FPP) and is Copyright (C)
3 * 2013-2024 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
17 #include <algorithm>
18 #include <chrono>
19 #include <climits>
20 #include <condition_variable>
21 #include <ctime>
22 #include <list>
23 #include <map>
24 #include <memory>
25 #include <mutex>
26 #include <semaphore>
27 #include <string>
28 #include <thread>
29 #include <unistd.h>
30 #include <utility>
31 #include <vector>
32
33 #include "OutputMonitor.h"
34 #include "Player.h"
35 #include "Warnings.h"
36 #include "common.h"
37 #include "fppversion.h"
38 #include "log.h"
39 #include "settings.h"
40 #include "commands/Commands.h"
41
42 #include "Events.h"
43
44 using namespace std::chrono;
45 using namespace std::chrono_literals;
46
47 static std::list<EventHandler*> EVENT_HANDLERS;
48 // Callbacks run on the event handler's thread (libmosquitto's for MQTT) while
49 // the main loop and API threads add and remove them -- plugins re-register on
50 // every config reload, and unload removes theirs before their code goes away.
51 // So the map is locked, a call runs from a slot the caller holds a reference
52 // to (never from the map), and RemoveCallback waits for calls in flight.
53 struct EventCallbackSlot {
54 std::function<void(const std::string& topic, const std::string& payload)> fn;
55 int inFlight = 0; // guarded by EVENT_CALLBACKS_LOCK
56 };
57 static std::mutex EVENT_CALLBACKS_LOCK;
58 static std::condition_variable EVENT_CALLBACKS_IDLE;
59 static std::map<std::string, std::shared_ptr<EventCallbackSlot>> EVENT_CALLBACKS;
60 static thread_local int tl_invokingCallback = 0;
61
62 72 static std::vector<std::string> EventCallbackTopics() {
63
1/2
✓ Branch 0 taken 72 times.
✗ Branch 1 not taken.
72 std::lock_guard<std::mutex> lock(EVENT_CALLBACKS_LOCK);
64 72 std::vector<std::string> topics;
65
2/2
✓ Branch 0 taken 432 times.
✓ Branch 1 taken 72 times.
504 for (auto& c : EVENT_CALLBACKS) {
66
1/2
✓ Branch 0 taken 432 times.
✗ Branch 1 not taken.
432 topics.push_back(c.first);
67 }
68 72 return topics;
69 72 }
70 // Intentionally leaked: if this were a plain file-static, __cxa_finalize would
71 // run ~thread() on ANY exit() call (early startup failures like a bridge-port
72 // bind error call exit(1) long before the orderly _exit(0) at the end of
73 // main()), and destroying a still-joinable std::thread calls std::terminate.
74 // Leaking the object means the destructor never runs; the orderly shutdown
75 // path still stops and joins the thread explicitly.
76 static std::thread& EVENT_PUBLISH_THREAD = *new std::thread();
77 static std::binary_semaphore EVENT_PUBLISH_THREAD_STOP{ 0 };
78 static std::vector<std::unique_ptr<EventNotifier>> eventNotifiers;
79
80 class PublishPlaylistStatus : public EventNotifier {
81 public:
82 2 PublishPlaylistStatus(int freq) :
83 2 EventNotifier(freq) {}
84 ✗ ~PublishPlaylistStatus() {}
85
86 4 void notify() {
87
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 Json::Value json = Player::INSTANCE.GetMqttStatusJSON();
88
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 std::stringstream buffer;
89
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 buffer << json << std::endl;
90
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 LogDebug(VB_CONTROL, "Notify PublishPlaylist\n");
91
3/6
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
12 Events::Publish("playlist_details", buffer.str());
92 4 }
93 };
94
95 extern void GetCurrentFPPDStatus(Json::Value& result);
96
97 class PublishFPPDStatus : public EventNotifier {
98 public:
99 1 PublishFPPDStatus(int freq) :
100 1 EventNotifier(freq) {}
101 ✗ ~PublishFPPDStatus() {}
102
103 5 void notify() {
104
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 Json::Value json;
105
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 GetCurrentFPPDStatus(json);
106
107
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 std::stringstream buffer;
108
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 buffer << json << std::endl;
109
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 LogDebug(VB_CONTROL, "Notify FPPDStatus\n");
110
3/6
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 5 times.
✗ Branch 5 not taken.
15 Events::Publish("fppd_status", buffer.str());
111 5 }
112 };
113
114 class PublishPortStatus : public EventNotifier {
115 public:
116 ✗ PublishPortStatus(int freq) :
117 ✗ EventNotifier(freq) {}
118 ✗ ~PublishPortStatus() {}
119
120 ✗ void notify() {
121 ✗ Json::Value json = Json::arrayValue;
122 ✗ OutputMonitor::INSTANCE.GetCurrentPortStatusJson(json);
123 ✗ std::stringstream buffer;
124 ✗ buffer << json << std::endl;
125 ✗ LogDebug(VB_CONTROL, "Notify FPPDStatus\n");
126 ✗ Events::Publish("port_status", buffer.str());
127 ✗ }
128 };
129
130 class EventWarningListener : public WarningListener {
131 public:
132 1885 EventWarningListener() {}
133 7468 virtual ~EventWarningListener() {}
134
135 2197 virtual void handleWarnings(const std::list<FPPWarning>& warnings) {
136
2/2
✓ Branch 0 taken 37 times.
✓ Branch 1 taken 2160 times.
2197 if (Events::HasEventHandlers()) {
137
1/2
✓ Branch 0 taken 37 times.
✗ Branch 1 not taken.
37 Json::Value rc = Json::Value(Json::arrayValue);
138
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 37 times.
44 for (auto& w : warnings) {
139
2/4
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
7 rc.append(w.toJsonValue());
140 }
141
2/4
✓ Branch 0 taken 37 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 37 times.
✗ Branch 3 not taken.
37 std::string msg = SaveJsonToString(rc);
142
1/2
✓ Branch 0 taken 37 times.
✗ Branch 1 not taken.
37 LogDebug(VB_CONTROL, "Sending warning message: %s\n", msg.c_str());
143
2/4
✓ Branch 0 taken 37 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 37 times.
✗ Branch 3 not taken.
37 Events::Publish("warnings", msg);
144 37 }
145 2197 }
146 };
147 static std::unique_ptr<EventWarningListener> EVENT_WARNING_LISTENER;
148
149 36 EventHandler::EventHandler() {}
150 72 EventHandler::~EventHandler() {}
151
152 1885 void Events::Ready() {
153
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 if (!EVENT_WARNING_LISTENER) {
154
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 EVENT_WARNING_LISTENER = std::make_unique<EventWarningListener>();
155
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 WarningHolder::AddWarningListener(EVENT_WARNING_LISTENER.get());
156 }
157
158
2/4
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1885 times.
✗ Branch 3 not taken.
3770 Events::Publish(MQTT_READY_TOPIC_NAME, 1);
159
4/8
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1885 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1885 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1885 times.
✗ Branch 7 not taken.
7540 Events::Publish("version", getFPPVersion());
160
4/8
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1885 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1885 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1885 times.
✗ Branch 7 not taken.
5655 Events::Publish("branch", getFPPBranch());
161
162
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 int playlistFrequency = getSettingInt("MQTTFrequency");
163
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 int statusFrequency = getSettingInt("MQTTStatusFrequency");
164
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 int portFrequency = getSettingInt("MQTTPortStatusFrequency");
165
166
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 eventNotifiers.reserve((playlistFrequency > 0) + (statusFrequency > 0) + (portFrequency > 0));
167
168
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1883 times.
1885 if (playlistFrequency > 0) {
169
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 eventNotifiers.push_back(std::make_unique<PublishPlaylistStatus>(playlistFrequency));
170 }
171
172
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1884 times.
1885 if (statusFrequency > 0) {
173
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 eventNotifiers.push_back(std::make_unique<PublishFPPDStatus>(statusFrequency));
174 }
175
176
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1885 times.
1885 if (portFrequency > 0) {
177 ✗ eventNotifiers.push_back(std::make_unique<PublishPortStatus>(portFrequency));
178 }
179
180
5/6
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 1882 times.
✓ Branch 4 taken 3 times.
✓ Branch 5 taken 1882 times.
1885 if (!EVENT_PUBLISH_THREAD.joinable() && !eventNotifiers.empty()) {
181 // create background Publish Thread
182
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 EVENT_PUBLISH_THREAD = std::thread(Events::RunPublishThread);
183 }
184 1885 }
185
186 36 void Events::AddEventHandler(EventHandler* handler) {
187
3/4
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 72 times.
✓ Branch 3 taken 36 times.
108 for (auto& topic : EventCallbackTopics()) {
188
1/2
✓ Branch 0 taken 72 times.
✗ Branch 1 not taken.
72 handler->RegisterCallback(topic);
189 36 }
190 36 EVENT_HANDLERS.push_back(handler);
191 36 }
192 36 void Events::RemoveEventHandler(EventHandler* handler) {
193
3/4
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 360 times.
✓ Branch 3 taken 36 times.
396 for (auto& topic : EventCallbackTopics()) {
194
1/2
✓ Branch 0 taken 360 times.
✗ Branch 1 not taken.
360 handler->RemoveCallback(topic);
195 36 }
196 36 EVENT_HANDLERS.remove(handler);
197 36 }
198
199 4084 bool Events::HasEventHandlers() {
200 4084 return !EVENT_HANDLERS.empty();
201 }
202
203 278542 bool Events::Publish(const std::string& topic, const std::string& data) {
204 278542 bool ret = true;
205
2/2
✓ Branch 0 taken 2145 times.
✓ Branch 1 taken 278542 times.
280687 for (auto handler : EVENT_HANDLERS) {
206
1/2
✓ Branch 0 taken 2145 times.
✗ Branch 1 not taken.
2145 ret &= handler->Publish(topic, data);
207 }
208 278542 return ret;
209 }
210
211 47729 bool Events::Publish(const std::string& topic, const int valueconst) {
212 47729 bool ret = true;
213
2/2
✓ Branch 0 taken 1286 times.
✓ Branch 1 taken 47729 times.
49015 for (auto handler : EVENT_HANDLERS) {
214
1/2
✓ Branch 0 taken 1286 times.
✗ Branch 1 not taken.
1286 ret &= handler->Publish(topic, valueconst);
215 }
216 47729 return ret;
217 }
218
219 13323 void Events::AddCallback(const std::string& topic, std::function<void(const std::string& topic, const std::string& payload)>& callback) {
220
1/2
✓ Branch 0 taken 13323 times.
✗ Branch 1 not taken.
13323 auto slot = std::make_shared<EventCallbackSlot>();
221
1/2
✓ Branch 0 taken 13323 times.
✗ Branch 1 not taken.
13323 slot->fn = callback;
222 {
223
1/2
✓ Branch 0 taken 13323 times.
✗ Branch 1 not taken.
13323 std::lock_guard<std::mutex> lock(EVENT_CALLBACKS_LOCK);
224
1/2
✓ Branch 0 taken 13323 times.
✗ Branch 1 not taken.
13323 EVENT_CALLBACKS[topic] = slot;
225 13323 }
226
2/2
✓ Branch 0 taken 289 times.
✓ Branch 1 taken 13323 times.
13612 for (auto handler : EVENT_HANDLERS) {
227
1/2
✓ Branch 0 taken 289 times.
✗ Branch 1 not taken.
289 handler->RegisterCallback(topic);
228 }
229 13323 }
230 1 void Events::RemoveCallback(const std::string& topic) {
231
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
2 for (auto handler : EVENT_HANDLERS) {
232
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 handler->RemoveCallback(topic);
233 }
234
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 std::unique_lock<std::mutex> lock(EVENT_CALLBACKS_LOCK);
235
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 auto it = EVENT_CALLBACKS.find(topic);
236
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (it == EVENT_CALLBACKS.end()) {
237 ✗ return;
238 }
239 1 std::shared_ptr<EventCallbackSlot> slot = it->second;
240
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 EVENT_CALLBACKS.erase(it);
241 // Wait for a call already running on the handler's thread to finish, so
242 // the caller can then free what the callback uses (or unload its
243 // plugin). Not from inside a callback: that would wait on itself. Not
244 // forever either: a callback stuck waiting on the remover's thread would
245 // otherwise deadlock both.
246
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
2 if (tl_invokingCallback == 0 &&
247
3/6
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 1 times.
4 !EVENT_CALLBACKS_IDLE.wait_for(lock, std::chrono::seconds(5), [&slot]() { return slot->inFlight == 0; })) {
248 ✗ LogWarn(VB_CONTROL, "Event callback for \"%s\" still running 5s after its removal\n", topic.c_str());
249 }
250
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 }
251 201 void Events::InvokeCallback(const std::string& topic, const std::string& topic_in, const std::string& payload) {
252 201 std::shared_ptr<EventCallbackSlot> slot;
253 {
254 // find(), not operator[]: an unknown topic would otherwise insert an
255 // empty entry.
256
1/2
✓ Branch 0 taken 201 times.
✗ Branch 1 not taken.
201 std::lock_guard<std::mutex> lock(EVENT_CALLBACKS_LOCK);
257
1/2
✓ Branch 0 taken 201 times.
✗ Branch 1 not taken.
201 auto it = EVENT_CALLBACKS.find(topic);
258
3/6
✓ Branch 0 taken 201 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 201 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 201 times.
201 if (it == EVENT_CALLBACKS.end() || !it->second->fn) {
259 ✗ return;
260 }
261 201 slot = it->second;
262 201 slot->inFlight++;
263
1/2
✓ Branch 0 taken 201 times.
✗ Branch 1 not taken.
201 }
264 // Callbacks run on the event handler's own thread (libmosquitto's for
265 // MQTT), where nothing above us catches - an exception escaping here is
266 // std::terminate. The payload is whatever a broker client sent.
267 201 tl_invokingCallback++;
268 try {
269
1/2
✓ Branch 0 taken 199 times.
✗ Branch 1 not taken.
201 slot->fn(topic_in, payload);
270 ✗ } catch (const std::exception& e) {
271 ✗ LogErr(VB_CONTROL, "Event callback for \"%s\" threw: %s\n", topic_in.c_str(), e.what());
272 ✗ } catch (...) {
273 ✗ LogErr(VB_CONTROL, "Event callback for \"%s\" threw an unknown exception\n", topic_in.c_str());
274 ✗ }
275 199 tl_invokingCallback--;
276 {
277
1/2
✓ Branch 0 taken 199 times.
✗ Branch 1 not taken.
199 std::lock_guard<std::mutex> lock(EVENT_CALLBACKS_LOCK);
278 199 slot->inFlight--;
279 199 }
280 199 EVENT_CALLBACKS_IDLE.notify_all();
281
1/2
✓ Branch 0 taken 199 times.
✗ Branch 1 not taken.
199 }
282
283 1867 void Events::PrepareForShutdown() {
284
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1864 times.
1867 if (EVENT_PUBLISH_THREAD.joinable()) {
285 3 EVENT_PUBLISH_THREAD_STOP.release();
286 3 EVENT_PUBLISH_THREAD.join();
287 }
288
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 if (EVENT_WARNING_LISTENER) {
289 1867 WarningHolder::RemoveWarningListener(EVENT_WARNING_LISTENER.get());
290 1867 EVENT_WARNING_LISTENER.reset();
291 }
292
293
2/4
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1867 times.
✗ Branch 3 not taken.
1867 Publish(MQTT_READY_TOPIC_NAME, 0);
294
2/2
✓ Branch 0 taken 36 times.
✓ Branch 1 taken 1867 times.
1903 for (auto handler : EVENT_HANDLERS) {
295
1/2
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
36 handler->PrepareForShutdown();
296 }
297 1867 }
298
299 3 void Events::RunPublishThread() {
300
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 SetThreadName("FPP-Events");
301
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 std::this_thread::sleep_for(3s); // Give everything time to start up
302
303
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 LogInfo(VB_CONTROL, "Starting Publish Thread\n");
304
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (eventNotifiers.empty()) {
305 // kill thread
306 ✗ LogInfo(VB_CONTROL, "Stopping Publish Thread as frequency is zero.\n");
307 ✗ return;
308 }
309
310 // Loop forever. We are running both publishes in one thread
311 // just to minimize the number of threads
312 3 seconds sleepDuration(0);
313 // Thread will exit when the semaphore can be acquired
314
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 3 times.
12 while (!EVENT_PUBLISH_THREAD_STOP.try_acquire_for(sleepDuration)) {
315 9 auto now = system_clock::now();
316
317 9 sleepDuration = seconds::max();
318
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 9 times.
18 for (auto& pNotifier : eventNotifiers) {
319 // next_time is wall-clock seconds (EventNotifier is plugin ABI, so
320 // its clock can't change). A due time more than one period away
321 // can only mean the clock stepped back: publishing would otherwise
322 // stop for the size of the step. Publish now and re-anchor.
323
4/6
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 8 times.
9 if (system_clock::from_time_t(pNotifier->next_time) > now + seconds(pNotifier->frequency)) {
324 1 pNotifier->next_time = system_clock::to_time_t(now);
325 }
326
2/4
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
9 if (system_clock::from_time_t(pNotifier->next_time) <= now) {
327
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 pNotifier->notify();
328
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 pNotifier->next_time = system_clock::to_time_t(now + seconds(pNotifier->frequency));
329 }
330
1/2
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
9 sleepDuration = std::min(sleepDuration, seconds(pNotifier->frequency));
331 }
332 }
333 }
334