GCC Code Coverage Report


Directory: ./
File: Warnings.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 155 168 92.3%
Functions: 21 23 91.3%
Branches: 161 286 56.3%

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 <algorithm>
16 #include <atomic>
17 #include <mutex>
18 #include <semaphore>
19 #include <set>
20 #include <shared_mutex>
21 #include <stdio.h>
22 #include <thread>
23 #include <utility>
24
25 #include "fpp-json.h" // Json::Value -- the serialization side of FPPWarning lives here
26
27 #include "Warnings.h"
28 #include "common.h"
29 #include "log.h"
30 #include "settings.h"
31
32 using namespace std::chrono;
33
34 namespace
35 {
36 std::list<FPPWarning> warnings;
37 std::shared_mutex warningsLock;
38 std::mutex listenerListLock;
39 std::set<WarningListener*> listenerList;
40 steady_clock::time_point wakeUpTime = steady_clock::time_point::max();
41 std::atomic_bool runNotifyThread{ false };
42 // Intentionally leaked: if this were a plain static, __cxa_finalize would
43 // run ~thread() on ANY exit() call (early startup failures call exit(1)
44 // long before the orderly _exit(0) at the end of main()), and destroying
45 // a still-joinable std::thread calls std::terminate. Leaking the object
46 // means the destructor never runs; StopNotifyThread still joins it on the
47 // orderly shutdown path.
48 std::thread& notifyThread = *new std::thread();
49 std::binary_semaphore sema{ 0 };
50 }
51
52 static void PruneWarnings();
53
54 1633 FPPWarning::FPPWarning(int i, const std::string& m, const std::string& p) :
55 1633 m_id(i),
56
1/2
✓ Branch 0 taken 1633 times.
✗ Branch 1 not taken.
1633 m_message(m),
57
1/2
✓ Branch 0 taken 1633 times.
✗ Branch 1 not taken.
3266 m_plugin(p) {
58 1633 timeout = steady_clock::time_point::max();
59 1633 }
60
61 25778 Json::Value FPPWarning::toJsonValue() const {
62 25778 Json::Value v;
63
2/4
✓ Branch 0 taken 25778 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 25778 times.
✗ Branch 3 not taken.
25778 v["id"] = m_id;
64
2/4
✓ Branch 0 taken 25778 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 25778 times.
✗ Branch 3 not taken.
25778 v["message"] = m_message;
65
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 25778 times.
25778 if (!m_plugin.empty()) {
66 ✗ v["plugin"] = m_plugin;
67 }
68
2/2
✓ Branch 0 taken 72 times.
✓ Branch 1 taken 25778 times.
25850 for (auto& kv : data) {
69
3/6
✓ Branch 0 taken 72 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 72 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 72 times.
✗ Branch 5 not taken.
72 v["data"][kv.first] = kv.second;
70 }
71 25778 return v;
72 ✗ }
73
74 3770 void WarningHolder::AddWarningListener(WarningListener* l) {
75
1/2
✓ Branch 0 taken 3770 times.
✗ Branch 1 not taken.
3770 LogDebug(VB_GENERAL, "About to add listener\n");
76
1/2
✓ Branch 0 taken 3770 times.
✗ Branch 1 not taken.
3770 std::unique_lock<std::mutex> lock(listenerListLock);
77
1/2
✓ Branch 0 taken 3770 times.
✗ Branch 1 not taken.
3770 listenerList.insert(l);
78
1/2
✓ Branch 0 taken 3770 times.
✗ Branch 1 not taken.
3770 LogDebug(VB_GENERAL, "Adding Listener Done\n");
79 3770 }
80
81 3734 void WarningHolder::RemoveWarningListener(WarningListener* l) {
82
1/2
✓ Branch 0 taken 3734 times.
✗ Branch 1 not taken.
3734 std::unique_lock<std::mutex> lock(listenerListLock);
83
2/4
✓ Branch 0 taken 3734 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3734 times.
✗ Branch 3 not taken.
3734 if (listenerList.erase(l)) {
84
1/2
✓ Branch 0 taken 3734 times.
✗ Branch 1 not taken.
3734 LogDebug(VB_GENERAL, "Removing Warning Listener Complete\n");
85 } else {
86 ✗ LogWarn(VB_GENERAL, "Requested to remove Listener, but it wasn't found\n");
87 }
88 3734 }
89
90 1888 void WarningHolder::StartNotifyThread() {
91 1888 runNotifyThread = true;
92
1/2
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
1888 notifyThread = std::thread(WarningHolder::WarningsThreadMain);
93 1888 }
94
95 1884 void WarningHolder::StopNotifyThread() {
96 1884 runNotifyThread = false;
97 1884 sema.release();
98 // Guard the join: StopNotifyThread() can run when the thread was never
99 // started (a crash signal arrives before StartNotifyThread) or has already
100 // been joined (normal shutdown joins it, then a later crash re-enters the
101 // signal handler). join() on a non-joinable thread throws std::system_error
102 // on both Linux and macOS -> terminate. joinable() makes it safe/idempotent.
103 // Nor from the notify thread itself (a crash inside a warnings listener
104 // reaches here on that thread): joining yourself throws, which would
105 // terminate from inside the crash handler.
106
5/6
✓ Branch 0 taken 1884 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1883 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 1883 times.
✓ Branch 5 taken 1 times.
1884 if (notifyThread.joinable() && notifyThread.get_id() != std::this_thread::get_id()) {
107 1883 notifyThread.join();
108 }
109 1884 }
110 4414 void WarningHolder::WriteWarningsFile() {
111
1/2
✓ Branch 0 taken 4414 times.
✗ Branch 1 not taken.
4414 Json::Value result = Json::Value(Json::ValueType::arrayValue);
112
1/2
✓ Branch 0 taken 4414 times.
✗ Branch 1 not taken.
4414 Json::Value resultFull = Json::Value(Json::ValueType::arrayValue);
113 {
114 // Only the copy is made under the lock. The writes below are file
115 // I/O on the SD card, and every AddWarning/RemoveWarning -- some on
116 // the output and media timing paths -- waits for this lock.
117
1/2
✓ Branch 0 taken 4414 times.
✗ Branch 1 not taken.
4414 std::shared_lock<std::shared_mutex> lock(warningsLock);
118
2/2
✓ Branch 0 taken 4854 times.
✓ Branch 1 taken 4414 times.
9268 for (auto& warn : warnings) {
119
2/4
✓ Branch 0 taken 4854 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4854 times.
✗ Branch 3 not taken.
4854 result.append(warn.message());
120
2/4
✓ Branch 0 taken 4854 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4854 times.
✗ Branch 3 not taken.
4854 resultFull.append(warn.toJsonValue());
121 }
122 4414 }
123
4/8
✓ Branch 0 taken 4414 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4414 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4414 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 4414 times.
✗ Branch 7 not taken.
17656 SaveJsonToFile(result, getFPPDDir("/www/warnings.json"));
124
4/8
✓ Branch 0 taken 4414 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4414 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4414 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 4414 times.
✗ Branch 7 not taken.
13242 SaveJsonToFile(resultFull, getFPPDDir("/www/warnings_full.json"));
125 4414 }
126 17 void WarningHolder::WriteCrashedWarningsFile(int id, const std::string& message) {
127
2/4
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
17 FPPWarning warn(id, message, "");
128
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 Json::Value result = Json::Value(Json::ValueType::arrayValue);
129
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 Json::Value resultFull = Json::Value(Json::ValueType::arrayValue);
130
2/4
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
17 result.append(warn.message());
131
2/4
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
17 resultFull.append(warn.toJsonValue());
132
4/8
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 17 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 17 times.
✗ Branch 7 not taken.
68 SaveJsonToFile(result, getFPPDDir("/www/warnings.json"));
133
4/8
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 17 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 17 times.
✗ Branch 7 not taken.
51 SaveJsonToFile(resultFull, getFPPDDir("/www/warnings_full.json"));
134 17 }
135 1867 void WarningHolder::ClearWarningsFile() {
136
2/4
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1867 times.
✗ Branch 3 not taken.
3734 remove(getFPPDDir("/www/warnings.json").c_str());
137
2/4
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1867 times.
✗ Branch 3 not taken.
1867 remove(getFPPDDir("/www/warnings_full.json").c_str());
138 1867 }
139
140 1888 void WarningHolder::WarningsThreadMain() {
141
2/4
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
1888 SetThreadName("FPP-Warnings");
142 1888 PruneWarnings();
143 1888 WriteWarningsFile();
144
2/2
✓ Branch 0 taken 4409 times.
✓ Branch 1 taken 2 times.
4411 while (runNotifyThread) {
145 // Read under warningsLock, which every writer of wakeUpTime holds.
146 4409 steady_clock::time_point wake;
147 {
148
1/2
✓ Branch 0 taken 4409 times.
✗ Branch 1 not taken.
4409 std::shared_lock<std::shared_mutex> lock(warningsLock);
149 4409 wake = wakeUpTime;
150 4409 }
151 4409 sema.try_acquire_until(wake);
152
2/2
✓ Branch 0 taken 1881 times.
✓ Branch 1 taken 2523 times.
4404 if (!runNotifyThread) {
153 1881 break;
154 }
155
1/2
✓ Branch 0 taken 2523 times.
✗ Branch 1 not taken.
2523 PruneWarnings();
156
1/2
✓ Branch 0 taken 2523 times.
✗ Branch 1 not taken.
2523 WriteWarningsFile();
157
158
1/2
✓ Branch 0 taken 2523 times.
✗ Branch 1 not taken.
2523 auto copy = GetWarnings();
159
160
1/2
✓ Branch 0 taken 2523 times.
✗ Branch 1 not taken.
2523 LogDebug(VB_GENERAL, "Warnings may have changed; notifying other threads of %zu Warnings\n", copy.size());
161
162
1/2
✓ Branch 0 taken 2523 times.
✗ Branch 1 not taken.
2523 std::unique_lock lock(listenerListLock);
163
1/2
✓ Branch 0 taken 2523 times.
✗ Branch 1 not taken.
7023 std::for_each(listenerList.begin(), listenerList.end(), [&](WarningListener* l) { l->handleWarnings(copy); });
164 2523 }
165 1883 }
166
167 30 void WarningHolder::AddWarning(const std::string& w) {
168
2/4
✓ Branch 0 taken 30 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 30 times.
✗ Branch 3 not taken.
30 AddWarningTimeout(-1, UNKNOWN_WARNING_ID, w);
169 30 }
170 9 void WarningHolder::AddWarningTimeout(const std::string& w, int seconds) {
171
2/4
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
9 AddWarningTimeout(seconds, UNKNOWN_WARNING_ID, w);
172 9 }
173 ✗ void WarningHolder::RemoveWarning(const std::string& w) {
174 ✗ RemoveWarning(UNKNOWN_WARNING_ID, w);
175 ✗ }
176 1039 void WarningHolder::AddWarning(int id, const std::string& w, const std::map<std::string, std::string>& data) {
177
2/4
✓ Branch 0 taken 1039 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1039 times.
✗ Branch 3 not taken.
1039 AddWarningTimeout(-1, id, w, data);
178 1039 }
179 3 void WarningHolder::AddWarning(int id, const std::string& w, const std::string& fixUrl, const std::string& fixText) {
180 3 std::map<std::string, std::string> data;
181
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 if (!fixUrl.empty()) {
182
3/6
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
6 data["fixUrl"] = fixUrl;
183 }
184
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 if (!fixText.empty()) {
185
3/6
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
6 data["fixText"] = fixText;
186 }
187
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 AddWarningTimeout(-1, id, w, data);
188 3 }
189
190 2600 void WarningHolder::AddWarningTimeout(int sec, int id, const std::string& w, const std::map<std::string, std::string>& data, const std::string& plugin) {
191
1/2
✓ Branch 0 taken 2600 times.
✗ Branch 1 not taken.
2600 LogDebug(VB_GENERAL, "Adding Warning: %s\n", w.c_str());
192 2600 steady_clock::time_point timeoutTime = steady_clock::time_point::max();
193
2/2
✓ Branch 0 taken 1528 times.
✓ Branch 1 taken 1072 times.
2600 if (sec != -1) {
194
1/2
✓ Branch 0 taken 1528 times.
✗ Branch 1 not taken.
1528 timeoutTime = steady_clock::now() + seconds(sec);
195 }
196
1/2
✓ Branch 0 taken 2600 times.
✗ Branch 1 not taken.
2600 std::unique_lock<std::shared_mutex> lock(warningsLock);
197
2/2
✓ Branch 0 taken 3482 times.
✓ Branch 1 taken 1616 times.
5098 for (auto it = warnings.begin(); it != warnings.end(); ++it) {
198
6/6
✓ Branch 0 taken 3233 times.
✓ Branch 1 taken 249 times.
✓ Branch 2 taken 984 times.
✓ Branch 3 taken 2249 times.
✓ Branch 4 taken 984 times.
✓ Branch 5 taken 2498 times.
3482 if (it->id() == id && it->message() == w) {
199 // Timeout is allowed to move later, but not earlier
200
3/4
✓ Branch 0 taken 984 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 563 times.
✓ Branch 3 taken 421 times.
984 if (timeoutTime > it->timeout)
201 563 it->timeout = timeoutTime;
202 984 return;
203 }
204 }
205
206
1/2
✓ Branch 0 taken 1616 times.
✗ Branch 1 not taken.
1616 auto& ref = warnings.emplace_back(id, w, plugin);
207
1/2
✓ Branch 0 taken 1616 times.
✗ Branch 1 not taken.
1616 ref.data = data;
208
209 1616 ref.timeout = timeoutTime;
210
3/4
✓ Branch 0 taken 1616 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 154 times.
✓ Branch 3 taken 1462 times.
1616 if (timeoutTime < wakeUpTime) {
211 154 wakeUpTime = timeoutTime;
212 }
213
1/2
✓ Branch 0 taken 1616 times.
✗ Branch 1 not taken.
1616 lock.unlock();
214
215 // Notify Listeners
216 1616 sema.release();
217
2/2
✓ Branch 0 taken 1616 times.
✓ Branch 1 taken 984 times.
2600 }
218 14779 void WarningHolder::RemoveWarning(int id, const std::string& w, const std::string& plugin) {
219
1/2
✓ Branch 0 taken 14779 times.
✗ Branch 1 not taken.
14779 LogDebug(VB_GENERAL, "Removing Warning: %s\n", w.c_str());
220
1/2
✓ Branch 0 taken 14779 times.
✗ Branch 1 not taken.
14779 std::unique_lock<std::shared_mutex> lock(warningsLock);
221
2/2
✓ Branch 0 taken 4258 times.
✓ Branch 1 taken 13758 times.
18016 for (auto it = warnings.begin(); it != warnings.end(); ++it) {
222
7/8
✓ Branch 0 taken 1299 times.
✓ Branch 1 taken 2959 times.
✓ Branch 2 taken 1021 times.
✓ Branch 3 taken 278 times.
✓ Branch 4 taken 1021 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1021 times.
✓ Branch 7 taken 3237 times.
4258 if (it->id() == id && it->message() == w && it->plugin() == plugin) {
223 1021 warnings.erase(it);
224
1/2
✓ Branch 0 taken 1021 times.
✗ Branch 1 not taken.
1021 lock.unlock();
225 // Notify Listeners
226 1021 sema.release();
227 1021 return;
228 }
229 }
230
2/2
✓ Branch 0 taken 13758 times.
✓ Branch 1 taken 1021 times.
14779 }
231 1917 void WarningHolder::UpdateLogLevelWarnings() {
232
3/6
✓ Branch 0 taken 1917 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1917 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1917 times.
✗ Branch 5 not taken.
7668 RemoveWarning(2, EXCESSIVE_LOG_LEVEL_WARNING);
233
3/6
✓ Branch 0 taken 1917 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1917 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1917 times.
✗ Branch 5 not taken.
5751 RemoveWarning(3, DEBUG_LOG_LEVEL_WARNING);
234 1917 int lowestLogLevel = FPPLogger::INSTANCE.MinimumLogLevel();
235
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 1909 times.
1917 if (lowestLogLevel == LOG_EXCESSIVE)
236
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
24 AddWarning(2, EXCESSIVE_LOG_LEVEL_WARNING);
237
2/2
✓ Branch 0 taken 60 times.
✓ Branch 1 taken 1849 times.
1909 else if (lowestLogLevel == LOG_DEBUG)
238
2/4
✓ Branch 0 taken 60 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 60 times.
✗ Branch 3 not taken.
166 AddWarning(3, DEBUG_LOG_LEVEL_WARNING);
239 1917 }
240
241 // Comes down by its timeout: a loader that still finds the entry raises it again.
242 static constexpr int INVALID_CONFIG_ENTRY_WARNING_ID = 73;
243
244 49 void WarningHolder::InvalidConfigEntry(const std::string& file, const std::string& detail) {
245 49 LogErr(VB_GENERAL, "%s: skipping an entry that could not be read (%s)\n", file.c_str(), detail.c_str());
246
4/8
✓ Branch 0 taken 49 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 49 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 49 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 49 times.
✗ Branch 7 not taken.
49 AddWarningTimeout(3600, INVALID_CONFIG_ENTRY_WARNING_ID, file + " has an entry that could not be read and was skipped: " + detail);
247 49 }
248
249 ✗ void WarningHolder::RemoveAllWarnings() {
250 ✗ std::unique_lock<std::shared_mutex> lock(warningsLock);
251 ✗ warnings.clear();
252 ✗ wakeUpTime = steady_clock::time_point::max();
253 ✗ lock.unlock();
254
255 // Notify Listeners
256 ✗ sema.release();
257 ✗ }
258
259 260190 std::list<FPPWarning> WarningHolder::GetWarnings() {
260
1/2
✓ Branch 0 taken 260190 times.
✗ Branch 1 not taken.
260190 std::shared_lock<std::shared_mutex> lock(warningsLock);
261
1/2
✓ Branch 0 taken 260190 times.
✗ Branch 1 not taken.
520380 return warnings;
262 260190 }
263
264 4411 void PruneWarnings() {
265 // warnings are not stored in a heap or sorted, so we must walk over the warnings to find expired
266
1/2
✓ Branch 0 taken 4411 times.
✗ Branch 1 not taken.
4411 std::unique_lock lock(warningsLock);
267 4411 wakeUpTime = steady_clock::time_point::max();
268
2/2
✓ Branch 0 taken 2424 times.
✓ Branch 1 taken 1987 times.
4411 if (!warnings.empty()) {
269 2424 auto now = steady_clock::now();
270
2/2
✓ Branch 0 taken 4845 times.
✓ Branch 1 taken 2424 times.
7269 for (auto it = warnings.begin(); it != warnings.end(); /* in loop */) {
271
3/4
✓ Branch 0 taken 4845 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 4842 times.
4845 if (it->timeout <= now) {
272 // warning has expired
273 3 it = warnings.erase(it);
274 3 continue;
275
3/4
✓ Branch 0 taken 4842 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1682 times.
✓ Branch 3 taken 3160 times.
4842 } else if (it->timeout < wakeUpTime) {
276 // our next warning
277 1682 wakeUpTime = it->timeout;
278 }
279 4842 ++it;
280 }
281 }
282 4411 }
283