GCC Code Coverage Report


Directory: ./
File: httpAPI.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 816 916 89.1%
Functions: 73 76 96.1%
Branches: 1421 2895 49.1%

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 drogon framework header before FPP headers to avoid
14 // macro conflicts between trantor's LOG_* macros and FPP's LogLevel enum
15 #include <drogon/HttpAppFramework.h>
16 #include <drogon/version.h>
17 #undef LOG_WARN
18 #undef LOG_INFO
19 #undef LOG_DEBUG
20
21 #include "fpp-pch.h"
22
23 #include "fpp-json.h"
24 #include "fpphttp.h" // drogon/HTTP helpers used here; no longer pulled transitively (see fpphttp_types.h)
25 #include "fpphttp_clientip.h" // getEffectiveClientIP() - FPP-internal, not in the plugin API
26 #include "StatusWebSocket.h"
27 #include <atomic>
28 #include <fcntl.h>
29 #include <signal.h>
30
31 // Defined in fppd.cpp; setting it to 0 breaks the main loop and triggers
32 // FPP's normal shutdown sequence, same as SIGQUIT or a graceful stop.
33 extern std::atomic_int runMainFPPDLoop;
34
35 #ifdef PLATFORM_OSX
36 #include <sys/sysctl.h>
37 #else
38 #include <sys/ioctl.h>
39 #include <sys/sysinfo.h>
40 #endif
41
42 #include <netinet/in.h>
43 #include <arpa/inet.h>
44
45 #include <cstdlib>
46 #include <ctime>
47 #include <cxxabi.h>
48 #include <iomanip>
49 #include <iostream>
50 #include <list>
51 #include <stdint.h>
52 #include <string>
53 #include <unistd.h>
54 #include <vector>
55
56 #include "MultiSync.h"
57 #include "OutputMonitor.h"
58 #include "mediaoutput/VideoInputManager.h"
59 #include "RecurringTasks.h"
60 #include "Player.h"
61 #include "Plugins.h"
62 #include "Timers.h"
63
64 #include <chrono>
65 #include <condition_variable>
66 #include <future>
67 #include <memory>
68 #include <mutex>
69 #include <trantor/net/EventLoop.h>
70 #include "Scheduler.h"
71 #include "Sequence.h"
72 #include "Variables.h"
73 #include "Warnings.h"
74 #include "common.h"
75 #include "e131bridge.h"
76 #include "effects.h"
77 #include "fppversion.h"
78 #include "gpio.h"
79 #include "log.h"
80 #include "mqtt.h"
81 #include "settings.h"
82 #include "channeloutput/ChannelOutputSetup.h"
83 #include "channeloutput/channeloutputthread.h"
84 #include "channeltester/ChannelTester.h"
85 #include "commands/Commands.h"
86 #include "commands/Condition.h"
87 #include "mediaoutput/AES67Manager.h"
88 #include "mediaoutput/AudioSourceRegistry.h"
89 #include "mediaoutput/AudioLevelMonitor.h"
90 #include "mediaoutput/MediaOutputBase.h"
91 #include "mediaoutput/MediaOutputStatus.h"
92 #include "mediaoutput/OpusRTPManager.h"
93 #include "mediaoutput/RTSPOutputManager.h"
94 #include "mediaoutput/StreamSlotManager.h"
95 #include "mediaoutput/mediaoutput.h"
96 #include "overlays/PixelOverlay.h"
97 #include "playlist/Playlist.h"
98 #include "sensors/Sensors.h"
99
100 #include "httpAPI.h"
101
102 // Monotonic, so the reported uptime survives a wall-clock step (NTP, an RTC
103 // that boots in the past) instead of going negative or jumping by hours.
104 static long long startupTimeMS = GetMonotonicTimeMS();
105 // Read by status requests on the HTTP threads, written by periodicWork() on
106 // the main loop.
107 static std::atomic<bool> piPowerBad{ false };
108
109 /*
110 Build a Status JSON String
111 */
112 137639 void GetCurrentFPPDStatus(Json::Value& result) {
113
1/2
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
137639 std::string UUID = getSetting("SystemUUID");
114
4/8
✓ Branch 0 taken 1885 times.
✓ Branch 1 taken 135754 times.
✓ Branch 2 taken 1885 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1885 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
137639 static std::string host_name = getSetting("HostName");
115
4/8
✓ Branch 0 taken 1885 times.
✓ Branch 1 taken 135754 times.
✓ Branch 2 taken 1885 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1885 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
137639 static std::string host_description = getSetting("HostDescription");
116
5/10
✓ Branch 0 taken 1885 times.
✓ Branch 1 taken 135754 times.
✓ 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.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
141409 static std::string fpp_version = getFPPVersion();
117
5/10
✓ Branch 0 taken 1885 times.
✓ Branch 1 taken 135754 times.
✓ 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.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
141409 static std::string fppd_branch = getFPPBranch();
118
4/8
✓ Branch 0 taken 1885 times.
✓ Branch 1 taken 135754 times.
✓ Branch 2 taken 1885 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1885 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
137639 static std::string platform = getPlatform();
119
120
1/2
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
137639 int mode = getFPPmode();
121
2/4
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 137639 times.
✗ Branch 3 not taken.
137639 result["fppd"] = "running";
122
2/4
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 137639 times.
✗ Branch 3 not taken.
137639 result["version"] = fpp_version;
123
2/4
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 137639 times.
✗ Branch 3 not taken.
137639 result["branch"] = fppd_branch;
124
2/4
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 137639 times.
✗ Branch 3 not taken.
137639 result["platform"] = platform;
125
2/4
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 137639 times.
✗ Branch 3 not taken.
137639 result["uuid"] = UUID;
126
2/4
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 137639 times.
✗ Branch 3 not taken.
137639 result["host_name"] = host_name;
127
2/4
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 137639 times.
✗ Branch 3 not taken.
137639 result["host_description"] = host_description;
128
2/4
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 137639 times.
✗ Branch 3 not taken.
137639 result["mode"] = mode;
129
5/10
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 137639 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 137639 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 137639 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 137639 times.
✗ Branch 9 not taken.
137639 result["mode_name"] = toStdStringAndFree(modeToString(getFPPmode()));
130
3/6
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 137639 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 137639 times.
✗ Branch 5 not taken.
137639 result["status"] = Player::INSTANCE.GetStatus();
131
3/6
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 137639 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 137639 times.
✗ Branch 5 not taken.
137639 result["bridging"] = HasBridgeData();
132
2/4
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 137639 times.
✗ Branch 3 not taken.
137639 result["multisync"] = multiSync->isMultiSyncEnabled();
133
2/4
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 137639 times.
✗ Branch 3 not taken.
137639 result["powerBad"] = piPowerBad.load();
134
135
3/6
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 137639 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 137639 times.
✗ Branch 5 not taken.
137639 result["channelInputsEnabled"] = InputsEnabled();
136
3/6
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 137639 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 137639 times.
✗ Branch 5 not taken.
137639 result["channelOutputsEnabled"] = HasUniverseOutputs();
137
138
3/4
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 498 times.
✓ Branch 3 taken 137141 times.
137639 if (ChannelTester::INSTANCE.Testing()) {
139
2/4
✓ Branch 0 taken 498 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 498 times.
✗ Branch 3 not taken.
498 result["status_name"] = "testing";
140 // Which test is running (same shape as GET api/testmode, minus any
141 // per-output "config" blob). Pushed over the status WebSocket, so
142 // every open page can track a test started or stopped elsewhere.
143
1/2
✓ Branch 0 taken 498 times.
✗ Branch 1 not taken.
498 Json::Value tm = ChannelTester::INSTANCE.GetStatusJson();
144
2/4
✓ Branch 0 taken 498 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 498 times.
✗ Branch 3 not taken.
498 if (!tm.isNull())
145
2/4
✓ Branch 0 taken 498 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 498 times.
✗ Branch 3 not taken.
498 result["testMode"] = tm;
146 498 } else {
147
8/11
✓ Branch 0 taken 137141 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 137141 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 54838 times.
✓ Branch 5 taken 75091 times.
✓ Branch 6 taken 138 times.
✓ Branch 7 taken 41 times.
✓ Branch 8 taken 88 times.
✓ Branch 9 taken 6945 times.
✗ Branch 10 not taken.
137141 switch (result["status"].asInt()) {
148 54838 case FPP_STATUS_IDLE:
149
2/4
✓ Branch 0 taken 54838 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 54838 times.
✗ Branch 3 not taken.
54838 result["status_name"] = "idle";
150 54838 break;
151 75091 case FPP_STATUS_PLAYLIST_PLAYING:
152
2/4
✓ Branch 0 taken 75091 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 75091 times.
✗ Branch 3 not taken.
75091 result["status_name"] = "playing";
153 75091 break;
154 138 case FPP_STATUS_STOPPING_GRACEFULLY:
155
2/4
✓ Branch 0 taken 138 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 138 times.
✗ Branch 3 not taken.
138 result["status_name"] = "stopping gracefully";
156 138 break;
157 41 case FPP_STATUS_STOPPING_GRACEFULLY_AFTER_LOOP:
158
2/4
✓ Branch 0 taken 41 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 41 times.
✗ Branch 3 not taken.
41 result["status_name"] = "stopping gracefully after loop";
159 41 break;
160 88 case FPP_STATUS_STOPPING_NOW:
161
2/4
✓ Branch 0 taken 88 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 88 times.
✗ Branch 3 not taken.
88 result["status_name"] = "stopping now";
162 88 break;
163 6945 case FPP_STATUS_PLAYLIST_PAUSED:
164
2/4
✓ Branch 0 taken 6945 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6945 times.
✗ Branch 3 not taken.
6945 result["status_name"] = "paused";
165 6945 break;
166 ✗ default:
167 ✗ result["status_name"] = "unknown";
168 }
169 }
170
171
3/6
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 137639 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 137639 times.
✗ Branch 5 not taken.
137639 result["volume"] = getVolume();
172
173 137639 auto t = std::time(nullptr);
174 struct tm tm;
175 137639 localtime_r(&t, &tm);
176
1/2
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
137639 std::stringstream sstr;
177
1/2
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
137639 sstr << std::put_time(&tm, "%a %b %d %H:%M:%S %Z %Y");
178
1/2
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
137639 std::string str = sstr.str();
179
2/4
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 137639 times.
✗ Branch 3 not taken.
137639 result["time"] = str;
180
181
1/2
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
137639 std::string fmt = getSetting("TimeFormat");
182
3/6
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 137639 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 137639 times.
✗ Branch 5 not taken.
548838 replaceAll(fmt, "%M", "%M:%S");
183
4/8
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 137639 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 137639 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 137639 times.
✗ Branch 7 not taken.
139357 result["timeStrFull"] = GetTimeStr(fmt.c_str());
184
4/8
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 137639 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 137639 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 137639 times.
✗ Branch 7 not taken.
137639 result["timeStr"] = GetTimeStr(getSetting("TimeFormat"));
185
4/8
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 137639 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 137639 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 137639 times.
✗ Branch 7 not taken.
137639 result["dateStr"] = GetDateStr(getSetting("DateFormat"));
186
187 // FPPD process uptime (time since fppd started)
188
1/2
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
137639 int totalseconds = (int)((GetMonotonicTimeMS() - startupTimeMS) / 1000);
189 137639 int days = totalseconds / 86400;
190 137639 int hours = (totalseconds % 86400) / 3600;
191 137639 int minutes = (totalseconds % 3600) / 60;
192 137639 int seconds = totalseconds % 60;
193
194
3/6
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 137639 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 137639 times.
✗ Branch 5 not taken.
137639 result["uptime"] = secondsToTime(totalseconds);
195
2/4
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 137639 times.
✗ Branch 3 not taken.
137639 result["uptimeTotalSeconds"] = totalseconds;
196
2/4
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 137639 times.
✗ Branch 3 not taken.
137639 result["uptimeSeconds"] = seconds;
197
2/4
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 137639 times.
✗ Branch 3 not taken.
137639 result["uptimeMinutes"] = minutes;
198
2/4
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 137639 times.
✗ Branch 3 not taken.
137639 result["uptimeHours"] = hours;
199
2/4
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 137639 times.
✗ Branch 3 not taken.
137639 result["uptimeDays"] = days;
200
201
1/2
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
137639 std::stringstream totalTime;
202
8/16
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 137639 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 137639 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 137639 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 137639 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 137639 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 137639 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 137639 times.
✗ Branch 15 not taken.
137639 totalTime << days << " days, " << hours << " hours, " << minutes << " minutes, " << seconds << " seconds";
203
3/6
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 137639 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 137639 times.
✗ Branch 5 not taken.
137639 result["uptimeStr"] = totalTime.str();
204
205 // System uptime
206 137639 int systemUptimeSecs = 0;
207 #ifdef PLATFORM_OSX
208 struct timeval boot;
209 struct timeval now;
210 int mib[] = { CTL_KERN, KERN_BOOTTIME };
211 size_t size = sizeof(boot);
212 if (sysctl(mib, 2, &boot, &size, 0, 0) == 0 && gettimeofday(&now, 0) == 0) {
213 systemUptimeSecs = (int)(now.tv_sec - boot.tv_sec);
214 }
215 #else
216 struct sysinfo sysInf;
217
1/2
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
137639 if (sysinfo(&sysInf) == 0) {
218 137639 systemUptimeSecs = (int)sysInf.uptime;
219 }
220 #endif
221
2/4
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 137639 times.
✗ Branch 3 not taken.
137639 result["systemUptimeTotalSeconds"] = systemUptimeSecs;
222
223
1/2
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
137639 Sensors::INSTANCE.reportSensors(result);
224
3/4
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20884 times.
✓ Branch 3 taken 137639 times.
158523 for (auto& warn : WarningHolder::GetWarnings()) {
225
3/6
✓ Branch 0 taken 20884 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20884 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 20884 times.
✗ Branch 5 not taken.
20884 result["warnings"].append(warn.message());
226
3/6
✓ Branch 0 taken 20884 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20884 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 20884 times.
✗ Branch 5 not taken.
20884 result["warningInfo"].append(warn.toJsonValue());
227 137639 }
228
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 137639 times.
137639 if (mode == 1) {
229 // bridge mode only returns the base information
230 ✗ return;
231 }
232
233 // MQTT
234
3/6
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 137639 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 137639 times.
✗ Branch 5 not taken.
137639 result["MQTT"]["configured"] = false;
235
3/6
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 137639 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 137639 times.
✗ Branch 5 not taken.
137639 result["MQTT"]["connected"] = false;
236
2/2
✓ Branch 0 taken 6099 times.
✓ Branch 1 taken 131540 times.
137639 if (mqtt) {
237
3/6
✓ Branch 0 taken 6099 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6099 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 6099 times.
✗ Branch 5 not taken.
6099 result["MQTT"]["configured"] = true;
238
4/8
✓ Branch 0 taken 6099 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6099 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 6099 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 6099 times.
✗ Branch 7 not taken.
6099 result["MQTT"]["connected"] = mqtt->IsConnected();
239 }
240
241
3/4
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 19217 times.
✓ Branch 3 taken 118422 times.
137639 if (getFPPmode() == REMOTE_MODE) {
242 19217 int secsElapsed = 0;
243 19217 int secsRemaining = 0;
244 19217 std::string seqFilename;
245 19217 std::string mediaFilename;
246
3/4
✓ Branch 0 taken 19217 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✓ Branch 3 taken 19210 times.
19217 if (sequence->IsSequenceRunning()) {
247
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 seqFilename = sequence->GetSeqFilenameCopy();
248 7 secsElapsed = sequence->m_seqMSElapsed / 1000;
249 7 secsRemaining = sequence->m_seqMSRemaining / 1000;
250 }
251 {
252 // CloseMediaOutput() deletes mediaOutput under mediaOutputLock at
253 // every media transition, and this status build runs on the HTTP
254 // threads, the drogon status-push timer and the warning notify
255 // thread. Copy the fields out under the lock rather than reading
256 // through the raw pointer while building the JSON. Nothing in here
257 // re-enters media code, so the hold is a string copy long.
258
1/2
✓ Branch 0 taken 19217 times.
✗ Branch 1 not taken.
19217 std::unique_lock<std::mutex> lock(mediaOutputLock);
259
2/2
✓ Branch 0 taken 19121 times.
✓ Branch 1 taken 96 times.
19217 if (mediaOutput) {
260
1/2
✓ Branch 0 taken 19121 times.
✗ Branch 1 not taken.
19121 mediaFilename = mediaOutput->m_mediaFilename;
261
1/2
✓ Branch 0 taken 19121 times.
✗ Branch 1 not taken.
19121 std::lock_guard<std::mutex> fl(MediaOutputStatus::FieldsLock());
262 19121 secsElapsed = mediaOutputStatus.secondsElapsed;
263 19121 secsRemaining = mediaOutputStatus.secondsRemaining;
264 19121 }
265 19217 }
266
267
8/10
✓ Branch 0 taken 19217 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 19210 times.
✓ Branch 3 taken 7 times.
✓ Branch 4 taken 19210 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 19121 times.
✓ Branch 7 taken 89 times.
✓ Branch 8 taken 19128 times.
✓ Branch 9 taken 89 times.
19217 if (seqFilename != "" || mediaFilename != "") {
268
2/4
✓ Branch 0 taken 19128 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 19128 times.
✗ Branch 3 not taken.
19128 result["status"] = 1;
269
2/4
✓ Branch 0 taken 19128 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 19128 times.
✗ Branch 3 not taken.
19128 result["status_name"] = "playing";
270 }
271
272
2/4
✓ Branch 0 taken 19217 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 19217 times.
✗ Branch 3 not taken.
19217 result["playlist"] = seqFilename;
273
2/4
✓ Branch 0 taken 19217 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 19217 times.
✗ Branch 3 not taken.
19217 result["sequence_filename"] = seqFilename;
274
3/6
✓ Branch 0 taken 19217 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 19217 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 19217 times.
✗ Branch 5 not taken.
19217 result["media_filename"] = mediaFilename.substr(mediaFilename.find_last_of("/\\") + 1);
275
2/4
✓ Branch 0 taken 19217 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 19217 times.
✗ Branch 3 not taken.
19217 result["current_sequence"] = seqFilename;
276
3/6
✓ Branch 0 taken 19217 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 19217 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 19217 times.
✗ Branch 5 not taken.
19217 result["current_song"] = result["media_filename"];
277
2/4
✓ Branch 0 taken 19217 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 19217 times.
✗ Branch 3 not taken.
19217 result["seconds_played"] = std::to_string(secsElapsed);
278
2/4
✓ Branch 0 taken 19217 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 19217 times.
✗ Branch 3 not taken.
19217 result["seconds_elapsed"] = std::to_string(secsElapsed);
279
2/4
✓ Branch 0 taken 19217 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 19217 times.
✗ Branch 3 not taken.
19217 result["seconds_remaining"] = std::to_string(secsRemaining);
280
3/6
✓ Branch 0 taken 19217 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 19217 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 19217 times.
✗ Branch 5 not taken.
19217 result["time_elapsed"] = secondsToTime(secsElapsed);
281
3/6
✓ Branch 0 taken 19217 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 19217 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 19217 times.
✗ Branch 5 not taken.
19217 result["time_remaining"] = secondsToTime(secsRemaining);
282 19217 } else {
283
1/2
✓ Branch 0 taken 118422 times.
✗ Branch 1 not taken.
118422 std::string NextPlaylist = scheduler->GetNextPlaylistName();
284
1/2
✓ Branch 0 taken 118422 times.
✗ Branch 1 not taken.
118422 std::string NextPlaylistStart = scheduler->GetNextPlaylistStartStr();
285
3/6
✓ Branch 0 taken 118422 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 118422 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 118422 times.
✗ Branch 5 not taken.
118422 result["next_playlist"]["playlist"] = NextPlaylist;
286
3/6
✓ Branch 0 taken 118422 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 118422 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 118422 times.
✗ Branch 5 not taken.
118422 result["next_playlist"]["start_time"] = NextPlaylistStart;
287
288
1/2
✓ Branch 0 taken 118422 times.
✗ Branch 1 not taken.
118422 Player::INSTANCE.GetCurrentStatus(result);
289
2/4
✓ Branch 0 taken 118422 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 118422 times.
✗ Branch 3 not taken.
118422 result["scheduler"] = scheduler->GetInfo();
290
291 // Add multi-stream slot status
292
2/4
✓ Branch 0 taken 118422 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 118422 times.
✗ Branch 3 not taken.
118422 Json::Value slots = StreamSlotManager::Instance().GetAllSlotsStatus();
293
2/4
✓ Branch 0 taken 118422 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 118422 times.
✗ Branch 3 not taken.
118422 result["streamSlots"] = slots;
294
295 // Media started outside a playlist -- "Play Media", a PSA, background
296 // music -- runs on a stream slot rather than through the player, so
297 // status_name read "idle" while audio was audibly playing. The slot
298 // data was already here; nothing looked at it. That cost real
299 // debugging time more than once: a box was declared silent on the
300 // strength of this field while a track was running.
301 //
302 // Only the display fields move. result["status"] deliberately stays
303 // FPP_STATUS_IDLE, because the scheduler gates on it in eight places
304 // and Player::StopNowAllLevels() stops levels while
305 // `GetStatus() != FPP_STATUS_IDLE`. It stops the playlist, not a
306 // slot, so reporting a slot as player activity would make it think
307 // a level was still playing.
308 118422 bool anyForeground = false, anyBackground = false;
309 118422 int lead = -1;
310
3/4
✓ Branch 0 taken 710532 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 592110 times.
✓ Branch 3 taken 118422 times.
710532 for (Json::ArrayIndex i = 0; i < slots.size(); i++) {
311
6/10
✓ Branch 0 taken 592110 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 592110 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 592110 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 592110 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 569201 times.
✓ Branch 9 taken 22909 times.
592110 if (slots[i]["status"].asString() != "playing") {
312 569201 continue;
313 }
314 // StreamSlot::isBackground exists but nothing currently sets it,
315 // so it cannot be the only test or this never fires. The manager
316 // already treats slots 2-5 as background everywhere else --
317 // StopBackgroundSlots() and ProcessBackgroundSlots() both operate
318 // on exactly that range -- so follow the same rule here, and keep
319 // honouring the flag for when it does get populated.
320
4/8
✓ Branch 0 taken 22909 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 22909 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 22909 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 22909 times.
✗ Branch 7 not taken.
45818 const bool bg = slots[i]["isBackground"].asBool() ||
321
5/8
✓ Branch 0 taken 22909 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 22909 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 22909 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 50 times.
✓ Branch 7 taken 22859 times.
22909 slots[i]["slot"].asInt() > 1;
322 22909 anyForeground |= !bg;
323 22909 anyBackground |= bg;
324 // Prefer a foreground slot as the one to describe; fall back to a
325 // background slot so a PSA or background music still names itself.
326
2/14
✗ Branch 0 not taken.
✓ Branch 1 taken 22909 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
✓ Branch 12 taken 22909 times.
✗ Branch 13 not taken.
22909 if (lead < 0 || (!bg && slots[lead]["isBackground"].asBool())) {
327 22909 lead = (int)i;
328 }
329 }
330
331
6/8
✓ Branch 0 taken 95563 times.
✓ Branch 1 taken 22859 times.
✓ Branch 2 taken 50 times.
✓ Branch 3 taken 95513 times.
✓ Branch 4 taken 118422 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 118422 times.
✗ Branch 7 not taken.
118422 result["media_playing"] = anyForeground || anyBackground;
332 // Only describe a stream slot when the player itself is idle. Slot 1
333 // IS the playlist's own media, so an active playlist always has a
334 // "playing" slot; overwriting status_name here would demote a
335 // "playing" playlist (sequence + media) to "playing media" and hide
336 // the sequence name on consumers such as the multisync page.
337 // Testing also sets its own status_name; leave that alone too.
338
6/8
✓ Branch 0 taken 22909 times.
✓ Branch 1 taken 95513 times.
✓ Branch 2 taken 22909 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 22909 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 702 times.
✓ Branch 7 taken 22207 times.
119124 if (lead >= 0 && result["status"].asInt() == FPP_STATUS_IDLE &&
339
8/14
✓ Branch 0 taken 702 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 702 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 702 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 702 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 702 times.
✓ Branch 9 taken 117720 times.
✓ Branch 10 taken 702 times.
✓ Branch 11 taken 117720 times.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
119124 result["status_name"].asString() == "idle") {
340
4/6
✓ Branch 0 taken 666 times.
✓ Branch 1 taken 36 times.
✓ Branch 2 taken 702 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 702 times.
✗ Branch 5 not taken.
702 result["status_name"] = anyForeground ? "playing media" : "playing background";
341
3/6
✓ Branch 0 taken 702 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 702 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 702 times.
✗ Branch 5 not taken.
702 const std::string f = slots[lead]["mediaFilename"].asString();
342
3/6
✓ Branch 0 taken 702 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 702 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 702 times.
✗ Branch 5 not taken.
702 result["current_song"] = f.substr(f.find_last_of("/\\") + 1);
343 // Only publish a position when the slot actually tracks one.
344 // Nothing drives slot 1's Process() when media is started outside
345 // a playlist -- both StreamSlotManager loops begin at slot 2,
346 // because slot 1 is normally the playlist's own media and the
347 // playlist pumps it -- so setMediaElapsed() never runs and these
348 // stay 0 for the whole track. Reporting that as a real position
349 // is worse than omitting it.
350
3/6
✓ Branch 0 taken 702 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 702 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 702 times.
✗ Branch 5 not taken.
702 const int elapsed = slots[lead]["secondsElapsed"].asInt();
351
3/6
✓ Branch 0 taken 702 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 702 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 702 times.
✗ Branch 5 not taken.
702 const int remaining = slots[lead]["secondsRemaining"].asInt();
352
4/4
✓ Branch 0 taken 237 times.
✓ Branch 1 taken 465 times.
✓ Branch 2 taken 167 times.
✓ Branch 3 taken 70 times.
702 if (elapsed > 0 || remaining > 0) {
353
2/4
✓ Branch 0 taken 632 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 632 times.
✗ Branch 3 not taken.
632 result["seconds_played"] = std::to_string(elapsed);
354
2/4
✓ Branch 0 taken 632 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 632 times.
✗ Branch 3 not taken.
632 result["seconds_remaining"] = std::to_string(remaining);
355 }
356 702 }
357
358 // A sequence started outside a playlist -- /api/sequence/.../start,
359 // the file manager's Play button, a "StartSequence" command preset --
360 // runs on the sequence engine, not through the player, so the idle
361 // branch above left current_sequence empty and status_name read
362 // "idle" (or "playing media" when media was also started on a slot)
363 // while channel data was visibly playing. Surface it the same way a
364 // playlist's sequence entry surfaces: status_name "playing" with
365 // current_sequence named, so the header badge and the multisync page
366 // agree on what is playing. A concurrent media slot keeps its
367 // current_song, so sequence + media outside a playlist names both.
368 //
369 // status goes with status_name, as it does for a playlist and for a
370 // remote playing a sequence: "playing" is FPP_STATUS_PLAYLIST_PLAYING.
371 // It was left at FPP_STATUS_IDLE, and the status page, which goes by
372 // status, said Idle beside a "playing" header badge. Only this JSON
373 // copy changes: the scheduler and Player::StopNowAllLevels() ask
374 // Player::GetStatus(), which stays idle.
375 //
376 // The allowlist matters: "testing" sets its own status_name above and
377 // must keep it, and the paused/stopping states are never idle, so
378 // only the three idle-family names can arrive here.
379
1/2
✓ Branch 0 taken 118422 times.
✗ Branch 1 not taken.
118422 if (result["status"].asInt() == FPP_STATUS_IDLE &&
380
7/12
✓ Branch 0 taken 36119 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 36119 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 36119 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1200 times.
✓ Branch 7 taken 34919 times.
✓ Branch 8 taken 5267 times.
✓ Branch 9 taken 113155 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
154541 (result["status_name"].asString() == "idle" ||
381
7/12
✓ Branch 0 taken 1200 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1200 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1200 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 534 times.
✓ Branch 7 taken 666 times.
✓ Branch 8 taken 36119 times.
✓ Branch 9 taken 82303 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
119622 result["status_name"].asString() == "playing media" ||
382
12/18
✓ Branch 0 taken 118422 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 36119 times.
✓ Branch 3 taken 82303 times.
✓ Branch 4 taken 534 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 534 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 534 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 36 times.
✓ Branch 11 taken 498 times.
✓ Branch 12 taken 534 times.
✓ Branch 13 taken 117888 times.
✓ Branch 14 taken 1200 times.
✓ Branch 15 taken 117222 times.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
272999 result["status_name"].asString() == "playing background") &&
383
3/4
✓ Branch 0 taken 35621 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5267 times.
✓ Branch 3 taken 30354 times.
35621 sequence->IsSequenceRunning()) {
384
1/2
✓ Branch 0 taken 5267 times.
✗ Branch 1 not taken.
5267 const std::string sf = sequence->GetSeqFilenameCopy();
385
2/4
✓ Branch 0 taken 5267 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5267 times.
✗ Branch 3 not taken.
5267 if (sf != "") {
386
3/6
✓ Branch 0 taken 5267 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5267 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 5267 times.
✗ Branch 5 not taken.
5267 result["current_sequence"] = sf.substr(sf.find_last_of("/\\") + 1);
387
2/4
✓ Branch 0 taken 5267 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5267 times.
✗ Branch 3 not taken.
5267 result["status"] = FPP_STATUS_PLAYLIST_PLAYING;
388
2/4
✓ Branch 0 taken 5267 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5267 times.
✗ Branch 3 not taken.
5267 result["status_name"] = "playing";
389 }
390 5267 }
391 118422 }
392
1/2
✓ Branch 0 taken 137639 times.
✗ Branch 1 not taken.
137639 }
393
394 /*
395 *
396 */
397 1886 APIServer::APIServer() {
398 1886 }
399
400 /*
401 *
402 */
403 // Set once fppd is stopping: the sync advice registered in Init() then turns
404 // every new request (and WebSocket upgrade) away before it reaches a router.
405 static std::atomic<bool> s_httpQuiescing{ false };
406
407 // drogon's quit() frees its routers on the main loop *before* it stops the IO
408 // loops: an IO thread still routing a request (the UI's status poll, say)
409 // then reads a freed router (TSan: StaticFileRouter::route() reading the
410 // default handler of a router the main loop just deleted). So before quit(),
411 // new requests are refused, and each IO loop is made to run a task: a loop
412 // runs queued tasks only between requests, so once every loop has run one,
413 // none is inside a router, and none will enter one again.
414 1867 static void QuiesceHttpServer() {
415 1867 s_httpQuiescing = true;
416
3/6
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1867 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 1867 times.
1867 if (!drogon::app().isRunning()) {
417 ✗ return;
418 }
419 struct Barrier {
420 std::mutex lock;
421 std::condition_variable done;
422 size_t left = 0;
423 };
424 // Shared with the tasks: one still queued when the wait gives up runs later.
425
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 auto barrier = std::make_shared<Barrier>();
426
2/4
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1867 times.
✗ Branch 3 not taken.
1867 size_t loops = drogon::app().getThreadNum();
427
2/2
✓ Branch 0 taken 18670 times.
✓ Branch 1 taken 1867 times.
20537 for (size_t i = 0; i < loops; i++) {
428
2/4
✓ Branch 0 taken 18670 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 18670 times.
✗ Branch 3 not taken.
18670 trantor::EventLoop* loop = drogon::app().getIOLoop(i);
429
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 18670 times.
18670 if (!loop) {
430 ✗ continue;
431 }
432 {
433
1/2
✓ Branch 0 taken 18670 times.
✗ Branch 1 not taken.
18670 std::lock_guard<std::mutex> l(barrier->lock);
434 18670 barrier->left++;
435 18670 }
436
2/4
✓ Branch 0 taken 18670 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 18670 times.
✗ Branch 3 not taken.
18670 loop->queueInLoop([barrier]() {
437
1/2
✓ Branch 0 taken 18670 times.
✗ Branch 1 not taken.
18670 std::lock_guard<std::mutex> l(barrier->lock);
438 18670 barrier->left--;
439 18670 barrier->done.notify_all();
440 18670 });
441 }
442
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 std::unique_lock<std::mutex> l(barrier->lock);
443
2/4
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1867 times.
6868 if (!barrier->done.wait_for(l, std::chrono::seconds(2), [&barrier]() { return barrier->left == 0; })) {
444 ✗ LogWarn(VB_HTTP, "%d HTTP IO loops still busy at shutdown\n", (int)barrier->left);
445 }
446 1867 }
447
448 1867 APIServer::~APIServer() {
449 // Before drogon stops: the status push registers a WarningHolder listener
450 // and holds connection pointers, both of which outlive quit() (which only
451 // queues teardown, so the close callbacks may never run). Init() turned it
452 // on from here, so it gets turned off from here too.
453 1867 StatusWebSocketShutdown();
454
455 1867 QuiesceHttpServer();
456 1867 drogon::app().quit();
457 // Join the server thread so Drogon is fully stopped before we return.
458 // PluginManager::Cleanup() (called from main() after MainLoop() exits)
459 // will dlclose() plugin libraries; if Drogon IO threads are still running
460 // at that point they may execute or release plugin handler lambdas whose
461 // code is in those libraries -> SEGV_MAPERR. Joining here ensures all
462 // Drogon threads have exited before any dlclose() can happen.
463
3/6
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1867 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1867 times.
✗ Branch 5 not taken.
1867 if (m_serverThread && m_serverThread->joinable()) {
464 1867 m_serverThread->join();
465 }
466
1/2
✓ Branch 0 taken 1867 times.
✗ Branch 1 not taken.
1867 delete m_serverThread;
467 1867 m_serverThread = nullptr;
468 1867 m_pr.reset();
469 1867 }
470
471 // drogon's registerHandler/registerHandlerViaRegex take the callable by
472 // rvalue reference and move out of it, so a handler that is registered on
473 // more than one route must be copied for each registration. Re-using the
474 // same variable would register a moved-from closure: harmless for the
475 // captureless lambdas below, but for capturing ones (e.g. handleFppd's
476 // shared_ptr) the later route ends up dereferencing a nulled capture and
477 // crashes. Every registration below goes through this explicit copy so
478 // adding a capture to any handler stays safe.
479 template<typename T>
480 113100 static T copyHandler(const T& handler) {
481 113100 return handler;
482 }
483
484 /*
485 *
486 */
487 // "1.9.13" -> "1.9"
488 4 static std::string drogonMajorMinor(const std::string& v) {
489 4 size_t first = v.find('.');
490
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 if (first == std::string::npos) {
491 ✗ return v;
492 }
493 4 size_t second = v.find('.', first + 1);
494
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 return second == std::string::npos ? v : v.substr(0, second);
495 }
496
497 // registerHandler() and friends are templates instantiated in fppd from the
498 // drogon headers it was compiled against, and they call into libdrogon by
499 // vtable slot. Built against one drogon's headers and run against another's
500 // library - headers from a drogon source install in /usr/local, library from
501 // the distro package, say - the first route registration jumps through the
502 // wrong slot and fppd segfaults on every start. drogon's SONAME stays
503 // libdrogon.so.1 across those releases, so the dynamic linker cannot catch it.
504 // Compare what we were built with against what we loaded, and refuse to run
505 // on a major.minor mismatch with a message that says what to fix. A patch
506 // difference (a distro update within a release) is only noted.
507 1886 static bool drogonVersionMatches() {
508
1/2
✓ Branch 0 taken 1886 times.
✗ Branch 1 not taken.
1886 std::string built = DROGON_VERSION;
509
1/2
✓ Branch 0 taken 1886 times.
✗ Branch 1 not taken.
1886 std::string loaded = drogon::getVersion();
510
2/2
✓ Branch 0 taken 1884 times.
✓ Branch 1 taken 2 times.
1886 if (built == loaded) {
511 1884 return true;
512 }
513
4/6
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 1 times.
2 if (drogonMajorMinor(built) != drogonMajorMinor(loaded)) {
514
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogErr(VB_HTTP, "fppd was built against drogon %s headers but loaded libdrogon %s. "
515 "These are not compatible; rebuild fppd against the drogon that is installed "
516 "(or remove the other drogon's headers or library)\n",
517 built.c_str(), loaded.c_str());
518
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 fprintf(stderr, "fppd: built against drogon %s headers but loaded libdrogon %s; rebuild fppd\n",
519 built.c_str(), loaded.c_str());
520 1 return false;
521 }
522
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogWarn(VB_HTTP, "fppd was built against drogon %s headers and loaded libdrogon %s\n",
523 built.c_str(), loaded.c_str());
524 1 return true;
525 1886 }
526
527 1886 void APIServer::Init(void) {
528
3/4
✓ Branch 0 taken 1886 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 1885 times.
1886 if (!drogonVersionMatches()) {
529 // _exit(), not exit(): fppd is half started, and the static
530 // destructors exit() runs can block on, or free memory under, the
531 // threads already running (see ExitWithoutTeardown() in fppd.cpp).
532
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 fflush(nullptr);
533 1 _exit(EXIT_FAILURE);
534 }
535
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 m_pr = std::make_shared<PlayerResource>();
536
537
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 auto& app = drogon::app();
538
539 // Register all handlers with path patterns that match the old libhttpserver routes.
540 // The "true" flag in the old register_resource() meant "family" (prefix match),
541 // so we register with {1} catch-all patterns.
542
543 // PlayerResource (/fppd/*)
544 224931 auto handleFppd = [pr = m_pr](const HttpRequestPtr& req,
545 std::function<void(const HttpResponsePtr&)>&& callback) {
546 223046 HttpResponsePtr resp;
547
7/10
✓ Branch 0 taken 223046 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6594 times.
✓ Branch 3 taken 216452 times.
✓ Branch 4 taken 6594 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 6594 times.
✓ Branch 8 taken 216452 times.
✓ Branch 9 taken 6594 times.
223046 if (req->method() == drogon::Get || req->isHead())
548
1/2
✓ Branch 0 taken 216452 times.
✗ Branch 1 not taken.
216452 resp = pr->render_GET(req);
549
3/4
✓ Branch 0 taken 6594 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1218 times.
✓ Branch 3 taken 5376 times.
6594 else if (req->method() == drogon::Post)
550
2/2
✓ Branch 0 taken 1048 times.
✓ Branch 1 taken 170 times.
1218 resp = pr->render_POST(req);
551
2/4
✓ Branch 0 taken 5376 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 5376 times.
5376 else if (req->method() == drogon::Put)
552 ✗ resp = pr->render_PUT(req);
553
2/4
✓ Branch 0 taken 5376 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5376 times.
✗ Branch 3 not taken.
5376 else if (req->method() == drogon::Delete)
554
1/2
✓ Branch 0 taken 5376 times.
✗ Branch 1 not taken.
5376 resp = pr->render_DELETE(req);
555 else
556 ✗ resp = makeStringResponse("Method Not Allowed", 405);
557
1/2
✓ Branch 0 taken 222876 times.
✗ Branch 1 not taken.
222876 callback(resp);
558 224931 };
559 // Register /fppd exact match first; the catch-all regex is registered after
560 // the more specific /fppd/* routes below so they take priority. Bare /fppd
561 // has no content of its own -- it 404s from PlayerResource just like a bad
562 // sub-path would (confirmed directly against fppd:32322). Intentionally
563 // left undocumented, same reasoning as bare /command below.
564
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.
11310 app.registerHandler("/fppd", copyHandler(handleFppd), {drogon::Get, drogon::Post, drogon::Put, drogon::Delete, drogon::Head});
565
566 // OutputMonitor (/fppd/ports/*)
567 213 auto handlePorts = [](const HttpRequestPtr& req,
568 std::function<void(const HttpResponsePtr&)>&& callback) {
569
2/4
✓ Branch 0 taken 213 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 213 times.
✗ Branch 3 not taken.
213 callback(OutputMonitor::INSTANCE.render_GET(req));
570 213 };
571
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.
11310 app.registerHandlerViaRegex("/fppd/ports", copyHandler(handlePorts), {drogon::Get, drogon::Head});
572
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.
11310 app.registerHandlerViaRegex("/fppd/ports/.*", copyHandler(handlePorts), {drogon::Get, drogon::Head});
573
574 // ChannelTester (/fppd/testing/*)
575 140 auto handleTesting = [](const HttpRequestPtr& req,
576 std::function<void(const HttpResponsePtr&)>&& callback) {
577 140 HttpResponsePtr resp;
578
7/10
✓ Branch 0 taken 140 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 26 times.
✓ Branch 3 taken 114 times.
✓ Branch 4 taken 26 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 26 times.
✓ Branch 8 taken 114 times.
✓ Branch 9 taken 26 times.
140 if (req->method() == drogon::Get || req->isHead())
579
1/2
✓ Branch 0 taken 114 times.
✗ Branch 1 not taken.
114 resp = ChannelTester::INSTANCE.render_GET(req);
580
2/4
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 26 times.
✗ Branch 3 not taken.
26 else if (req->method() == drogon::Post)
581
2/2
✓ Branch 0 taken 25 times.
✓ Branch 1 taken 1 times.
26 resp = ChannelTester::INSTANCE.render_POST(req);
582 else
583 ✗ resp = makeStringResponse("Method Not Allowed", 405);
584
1/2
✓ Branch 0 taken 139 times.
✗ Branch 1 not taken.
139 callback(resp);
585 140 };
586
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.
11310 app.registerHandlerViaRegex("/fppd/testing(/.*)?", copyHandler(handleTesting), {drogon::Get, drogon::Post, drogon::Head});
587
588 #ifdef HAS_AES67_GSTREAMER
589 /**
590 * Get AES67 status: whether it's active, PTP sync state, discovered
591 * streams and pipelines.
592 *
593 * @route GET /api/aes67
594 * @response 200 AES67 status.
595 */
596 // AES67 status/test endpoint
597 1382 auto handleAES67 = [](const HttpRequestPtr& req,
598 std::function<void(const HttpResponsePtr&)>&& callback) {
599
2/4
✓ Branch 0 taken 1382 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1382 times.
✗ Branch 3 not taken.
1382 callback(AES67Manager::INSTANCE.render_GET(req));
600 1382 };
601
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.
11310 app.registerHandler("/aes67", copyHandler(handleAES67), {drogon::Get, drogon::Head});
602
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.
11310 app.registerHandlerViaRegex("/aes67/.*", copyHandler(handleAES67), {drogon::Get, drogon::Head});
603 #endif
604
605 #ifdef HAS_OPUS_RTP_GSTREAMER
606 /**
607 * Get Opus RTP status: whether it's active and the current pipelines.
608 *
609 * @route GET /api/opusrtp
610 * @response 200 Opus RTP status.
611 */
612 584 auto handleOpusRTP = [](const HttpRequestPtr& req,
613 std::function<void(const HttpResponsePtr&)>&& callback) {
614
2/4
✓ Branch 0 taken 584 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 584 times.
✗ Branch 3 not taken.
584 callback(OpusRTPManager::INSTANCE.render_GET(req));
615 584 };
616
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.
11310 app.registerHandler("/opusrtp", copyHandler(handleOpusRTP), {drogon::Get, drogon::Head});
617
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.
11310 app.registerHandlerViaRegex("/opusrtp/.*", copyHandler(handleOpusRTP), {drogon::Get, drogon::Head});
618 #endif
619
620 #ifdef HAS_RTSP_OUTPUT_GSTREAMER
621 /**
622 * Get RTSP output status: whether the server is running, its port, and
623 * the URL and audio state of each configured mount.
624 *
625 * @route GET /api/rtspoutput
626 * @response 200 RTSP output status.
627 */
628 20 auto handleRTSPOutput = [](const HttpRequestPtr& req,
629 std::function<void(const HttpResponsePtr&)>&& callback) {
630
2/4
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
✗ Branch 3 not taken.
20 callback(RTSPOutputManager::INSTANCE.render_GET(req));
631 20 };
632
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.
11310 app.registerHandler("/rtspoutput", copyHandler(handleRTSPOutput), {drogon::Get, drogon::Head});
633
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.
11310 app.registerHandlerViaRegex("/rtspoutput/.*", copyHandler(handleRTSPOutput), {drogon::Get, drogon::Head});
634 #endif
635
636 #ifdef HAS_AUDIO_LEVEL_MONITOR
637 // Subscribe to live signal levels for a set of PipeWire nodes. Internal to
638 // fppd -- the browser reaches it through the PHP passthrough at
639 // /api/pipewire/audio/meters, and reads the levels themselves off the
640 // /fppdws WebSocket rather than polling for them.
641 //
642 // Body: {"nodes":[{"name":"fpp_group_x","sink":true}, ...], "ttl": 6000}
643 // "sink" says how to capture the node -- see AudioLevelMonitor::StartMeter.
644 //
645 // Metering is kept alive by time rather than by a connection: a caller
646 // re-posts to hold it open, so a browser that goes away silently stops
647 // costing anything once the TTL lapses. An empty list stops it at once.
648 12 auto handleMeters = [](const HttpRequestPtr& req,
649 std::function<void(const HttpResponsePtr&)>&& callback) {
650
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 Json::Value body;
651 12 std::vector<AudioLevelMonitor::Target> nodes;
652 12 int ttl = 6000;
653
4/8
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 12 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 12 times.
✗ Branch 7 not taken.
12 if (LoadJsonFromString(std::string(req->getBody()), body)) {
654
6/12
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 12 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 12 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 12 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 12 times.
✗ Branch 11 not taken.
12 if (body.isMember("nodes") && body["nodes"].isArray()) {
655
8/14
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 12 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 21 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 21 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 33 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 21 times.
✓ Branch 13 taken 12 times.
33 for (const auto& n : body["nodes"]) {
656 21 AudioLevelMonitor::Target t;
657 // Objects carry the node's kind; a bare string is assumed
658 // to be a sink, which is what most metered nodes are.
659
2/4
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 21 times.
✗ Branch 3 not taken.
21 if (n.isObject()) {
660
3/6
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 21 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 21 times.
✗ Branch 5 not taken.
21 t.name = n.get("name", "").asString();
661
3/6
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 21 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 21 times.
✗ Branch 5 not taken.
21 t.isSink = n.get("sink", true).asBool();
662 } else {
663 ✗ t.name = n.asString();
664 }
665
1/2
✓ Branch 0 taken 21 times.
✗ Branch 1 not taken.
21 nodes.push_back(t);
666 21 }
667 }
668
2/4
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
12 if (body.isMember("ttl")) {
669
2/4
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
12 ttl = body["ttl"].asInt();
670 }
671 }
672
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 AudioLevelMonitor::INSTANCE.Subscribe(nodes, ttl);
673
4/8
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 12 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 12 times.
✗ Branch 7 not taken.
36 callback(makeStringResponse("{\"status\":\"OK\"}", 200, "application/json"));
674 12 };
675
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.
11310 app.registerHandler("/fppd/meters", copyHandler(handleMeters), {drogon::Post});
676 #endif
677
678 // Plugin-published PipeWire audio sources (AudioSourceRegistry). Internal
679 // to fppd -- the publicly documented route is the PHP passthrough,
680 // GetPipeWirePluginSources() in www/api/controllers/pipewire.php, which
681 // is what actually sits at /api/pipewire/audio/plugin-sources (this raw
682 // fppd path isn't Apache-proxied directly, so it has no /api/ route of
683 // its own to document here).
684 2 auto handlePluginSources = [](const HttpRequestPtr& req,
685 std::function<void(const HttpResponsePtr&)>&& callback) {
686
3/6
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
4 std::string body = SaveJsonToString(AudioSourceRegistry::INSTANCE.toJson());
687
3/6
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
2 callback(makeStringResponse(body, 200, "application/json"));
688 2 };
689
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.
11310 app.registerHandler("/pipewire/audio/plugin-sources", copyHandler(handlePluginSources), {drogon::Get, drogon::Head});
690
691 // Internal-only endpoint, deliberately undocumented (no @route
692 // docblock) -- lists every HTTP route currently registered with the
693 // embedded Drogon server, FPP's own included, plus the loaded plugins'
694 // routes from FPP's plugin route registry, so ServeOpenApiSpec() in
695 // www/api/index.php can diff it against known @route docblocks and
696 // surface whatever's left over as plugin-supplied (e.g. fpp-brightness's
697 // /Brightness). Registered under /internal, not /fppd, specifically so
698 // it does NOT appear in Apache's direct-proxy namespace whitelist
699 // (etc/apache2.site) -- PHP reaches it the same way it reaches every
700 // other fppd endpoint, a direct call to localhost:32322, never through
701 // Apache. It's still technically reachable externally via the generic
702 // /api/plugin-apis/<path> passthrough (that rule proxies any fppd path
703 // unconditionally -- it's what makes third-party plugin routes work at
704 // all, so it can't be restricted just for this one path), but it's no
705 // longer listed anywhere or reachable at a guessable /api/ path.
706 ✗ auto handlePluginApiRoutes = [](const HttpRequestPtr& req,
707 std::function<void(const HttpResponsePtr&)>&& callback) {
708 static const char* methodNames[] = {"GET", "POST", "HEAD", "PUT", "DELETE", "OPTIONS", "PATCH", "INVALID"};
709 ✗ Json::Value result(Json::arrayValue);
710 ✗ for (auto& info : drogon::app().getHandlersInfo()) {
711 ✗ Json::Value entry;
712 ✗ entry["path"] = std::get<0>(info);
713 ✗ drogon::HttpMethod m = std::get<1>(info);
714 ✗ entry["method"] = (m >= 0 && m <= drogon::Invalid) ? methodNames[m] : "UNKNOWN";
715 ✗ entry["description"] = std::get<2>(info);
716 ✗ result.append(entry);
717 ✗ }
718 // Plugin paths are dispatched by FPP, not registered with drogon.
719 ✗ for (auto& [path, m] : FPPPlugins::listPluginApiRoutes()) {
720 ✗ Json::Value entry;
721 ✗ entry["path"] = path;
722 ✗ entry["method"] = (m >= 0 && m <= drogon::Invalid) ? methodNames[m] : "UNKNOWN";
723 ✗ entry["description"] = "";
724 ✗ result.append(entry);
725 ✗ }
726 ✗ callback(makeStringResponse(SaveJsonToString(result), 200, "application/json"));
727 ✗ };
728
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.
11310 app.registerHandler("/internal/pluginApiRoutes", copyHandler(handlePluginApiRoutes), {drogon::Get, drogon::Head});
729
730 // Builds a -manual crash report for POST /api/crashes/report. The build
731 // runs off the event loop.
732
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.
11310 app.registerHandler("/internal/crashReport", [](const HttpRequestPtr&, std::function<void(const HttpResponsePtr&)>&& callback) {
733 8 std::thread([callback = std::move(callback)]() {
734
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 Json::Value result;
735 4 std::string error;
736
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
4 std::string file = BuildManualCrashReport(error);
737
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1 times.
3 if (file.empty()) {
738
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 result["Status"] = "ERROR";
739
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 result["Code"] = error;
740 } else {
741
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 result["Status"] = "OK";
742
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 result["File"] = file;
743 }
744 // fppd may have stopped drogon while the report was building
745
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.
3 if (drogon::app().isRunning()) {
746
5/10
✓ 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.
✓ Branch 6 taken 3 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 3 times.
✗ Branch 9 not taken.
15 callback(makeStringResponse(SaveJsonToString(result), 200, "application/json"));
747 }
748
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
7 }).detach();
749 4 }, {drogon::Post});
750
751 /**
752 * Load or unload a plugin without restarting fppd, so installing a plugin can
753 * take effect and uninstalling one can stop having effect mid-show.
754 *
755 * PluginManager mutates state the main loop walks every tick, and unloading
756 * calls into the plugin to stop its threads, so the work is handed to the
757 * main loop via a one-shot timer and this thread waits for the result. The
758 * wait is bounded: a wedged main loop returns 503 rather than pinning one of
759 * drogon's small pool of I/O threads forever.
760 *
761 * Documented under readable parameter names rather than the positional
762 * {1}/{2} drogon registers it as - see the skip list in docs.php.
763 *
764 * @route POST /api/fppd/plugin/{PluginName}/{action}
765 * @response 200 The plugin was loaded or unloaded.
766 * ```json
767 * {"Status": "OK", "Message": "", "plugin": "fpp-brightness", "loaded": false}
768 * ```
769 * @response 503 The main loop did not answer in time; nothing was changed.
770 */
771 80 auto handlePluginLifecycle = [](const HttpRequestPtr& req,
772 std::function<void(const HttpResponsePtr&)>&& callback,
773 const std::string& name, const std::string& action) {
774
1/2
✓ Branch 0 taken 80 times.
✗ Branch 1 not taken.
80 std::string clientIP = getEffectiveClientIP(req);
775 // name and action are raw URL path segments - unvalidated at this point
776 // (action isn't checked against "load" until below, and the reject branch
777 // is by definition reached with a malformed name), so truncate before
778 // logging rather than let a caller choose how many bytes of fppd.log a
779 // single rejected request consumes.
780 // The name becomes a path under the plugin directory, so "." and ".."
781 // would name the plugin directory itself or the media directory above
782 // it. Same rule as the PHP plugin API: ^[A-Za-z0-9_.-]+$, less those
783 // two.
784
7/10
✓ Branch 0 taken 80 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 80 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 78 times.
✓ Branch 5 taken 2 times.
✓ Branch 6 taken 78 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 77 times.
✓ Branch 9 taken 1 times.
80 bool validName = !name.empty() && name != "." && name != "..";
785
2/2
✓ Branch 0 taken 690 times.
✓ Branch 1 taken 77 times.
767 for (char c : name) {
786
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 687 times.
690 if (!validName) {
787 3 break;
788 }
789
6/12
✓ Branch 0 taken 595 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 595 times.
✓ Branch 3 taken 92 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 595 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 92 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✓ Branch 10 taken 92 times.
✗ Branch 11 not taken.
1374 validName = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') ||
790
4/6
✓ Branch 0 taken 595 times.
✓ Branch 1 taken 92 times.
✓ Branch 2 taken 92 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 92 times.
✗ Branch 5 not taken.
1374 c == '_' || c == '.' || c == '-';
791 }
792
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 77 times.
80 if (!validName) {
793
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.
3 LogWarn(VB_PLUGIN, "API - Rejected plugin %s request for invalid plugin name \"%s\" from %s\n",
794 TruncateForLog(action).c_str(), TruncateForLog(name).c_str(), clientIP.c_str());
795
4/8
✓ 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.
✓ Branch 6 taken 3 times.
✗ Branch 7 not taken.
9 callback(makeStringResponse("{\"Status\":\"ERROR\",\"Message\":\"invalid plugin name\"}",
796 400, "application/json"));
797 3 return;
798 }
799
1/2
✓ Branch 0 taken 77 times.
✗ Branch 1 not taken.
77 bool load = (action == "load");
800
4/6
✓ Branch 0 taken 77 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 38 times.
✓ Branch 3 taken 39 times.
✓ Branch 4 taken 77 times.
✗ Branch 5 not taken.
77 LogInfo(VB_PLUGIN, "API - %s plugin \"%s\" requested from %s\n",
801 load ? "Loading" : "Unloading", TruncateForLog(name).c_str(), clientIP.c_str());
802 // Timers replace a same-named entry, so a second request arriving while
803 // one is queued would silently drop the first. Keep the names distinct.
804 static std::atomic<uint64_t> seq{ 0 };
805
2/4
✓ Branch 0 taken 77 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 77 times.
✗ Branch 3 not taken.
77 std::string timerName = "PluginLifecycle-" + std::to_string(seq++);
806
807 // The result carries whether the plugin is loaded afterwards too: the
808 // plugin lists are the main loop's, so they are read there, not here.
809
1/2
✓ Branch 0 taken 77 times.
✗ Branch 1 not taken.
77 auto result = std::make_shared<std::promise<std::tuple<bool, std::string, bool>>>();
810
1/2
✓ Branch 0 taken 77 times.
✗ Branch 1 not taken.
77 auto done = result->get_future();
811
4/12
✓ Branch 0 taken 77 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 77 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 77 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 77 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
77 Timers::INSTANCE.addTimer(timerName, GetTimeMS(), [result, name, load]() {
812 // This runs plugin code (constructor, registerApis, shutdown,
813 // destructor) on the main loop, where nothing else catches.
814 77 std::string err;
815 77 bool ok = false;
816 try {
817
4/4
✓ Branch 0 taken 38 times.
✓ Branch 1 taken 39 times.
✓ Branch 2 taken 37 times.
✓ Branch 3 taken 1 times.
116 ok = load ? PluginManager::INSTANCE.loadPlugin(name, err)
818
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
39 : PluginManager::INSTANCE.unloadPlugin(name, err);
819
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 } catch (const std::exception& ex) {
820
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 err = std::string("plugin threw: ") + ex.what();
821
3/6
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
1 LogErr(VB_PLUGIN, "%s plugin \"%s\" threw: %s\n", load ? "Loading" : "Unloading",
822 TruncateForLog(name).c_str(), ex.what());
823 1 } catch (...) {
824 ✗ err = "plugin threw a non-standard exception";
825 ✗ LogErr(VB_PLUGIN, "%s plugin \"%s\" threw a non-standard exception\n", load ? "Loading" : "Unloading",
826 TruncateForLog(name).c_str());
827 ✗ }
828
3/6
✓ Branch 0 taken 77 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 77 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 77 times.
✗ Branch 5 not taken.
77 result->set_value({ ok, err, PluginManager::INSTANCE.isPluginLoaded(name) });
829 77 });
830
831
2/4
✓ Branch 0 taken 77 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 77 times.
77 if (done.wait_for(std::chrono::seconds(30)) != std::future_status::ready) {
832 ✗ LogErr(VB_PLUGIN, "API - %s plugin \"%s\" (from %s) timed out waiting for the main loop\n",
833 load ? "Loading" : "Unloading", TruncateForLog(name).c_str(), clientIP.c_str());
834 ✗ callback(makeStringResponse("{\"Status\":\"ERROR\",\"Message\":\"timed out waiting for the main loop\"}",
835 503, "application/json"));
836 ✗ return;
837 }
838
1/2
✓ Branch 0 taken 77 times.
✗ Branch 1 not taken.
77 auto [ok, err, loaded] = done.get();
839 // err can carry dlerror() text, which is not length-bounded either.
840
9/12
✓ Branch 0 taken 77 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 73 times.
✓ Branch 3 taken 4 times.
✓ Branch 4 taken 73 times.
✓ Branch 5 taken 4 times.
✓ Branch 6 taken 77 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 38 times.
✓ Branch 9 taken 39 times.
✓ Branch 10 taken 77 times.
✗ Branch 11 not taken.
77 LogInfo(VB_PLUGIN, "API - %s plugin \"%s\" (from %s): %s%s%s\n",
841 load ? "Load" : "Unload", TruncateForLog(name).c_str(), clientIP.c_str(),
842 ok ? "OK" : "FAILED", err.empty() ? "" : " - ", TruncateForLog(err).c_str());
843
1/2
✓ Branch 0 taken 77 times.
✗ Branch 1 not taken.
77 Json::Value resp(Json::objectValue);
844
4/6
✓ Branch 0 taken 73 times.
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 77 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 77 times.
✗ Branch 5 not taken.
77 resp["Status"] = ok ? "OK" : "ERROR";
845
2/4
✓ Branch 0 taken 77 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 77 times.
✗ Branch 3 not taken.
77 resp["Message"] = err;
846
2/4
✓ Branch 0 taken 77 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 77 times.
✗ Branch 3 not taken.
77 resp["plugin"] = name;
847
2/4
✓ Branch 0 taken 77 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 77 times.
✗ Branch 3 not taken.
77 resp["loaded"] = loaded;
848
7/12
✓ Branch 0 taken 77 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 73 times.
✓ Branch 3 taken 4 times.
✓ Branch 4 taken 77 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 77 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 77 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 77 times.
✗ Branch 11 not taken.
231 callback(makeStringResponse(SaveJsonToString(resp), ok ? 200 : 400, "application/json"));
849
5/8
✓ Branch 0 taken 77 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 77 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 77 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 77 times.
✓ Branch 7 taken 3 times.
80 };
850
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.
11310 app.registerHandler("/fppd/plugin/{1}/{2}", handlePluginLifecycle, {drogon::Post});
851
852 // PlayerResource catch-all for all other /fppd/* paths (registered AFTER
853 // specific /fppd/ports and /fppd/testing routes so they match first)
854
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.
11310 app.registerHandlerViaRegex("/fppd/.*", copyHandler(handleFppd), {drogon::Get, drogon::Post, drogon::Put, drogon::Delete, drogon::Head});
855
856 /**
857 * List/manage pixel overlay models. See /api/models/* and /api/overlays/*
858 * for per-model operations.
859 *
860 * @route GET /api/models
861 * @response 200 Array of overlay models.
862 */
863 // PixelOverlayManager (/models/*, /overlays/*). NOTE: despite the shared
864 // handler, methods are NOT uniformly supported across paths -- render_POST()
865 // only acts on p1=="models" and render_PUT() only acts on p1=="overlays"
866 // with a /model/{id}/... suffix (bare PUT /api/models 404s: "PUT Not
867 // found /models"). Confirmed live; verify before adding more @route
868 // docblocks here rather than assuming symmetry.
869 627033 auto handleModels = [](const HttpRequestPtr& req,
870 std::function<void(const HttpResponsePtr&)>&& callback) {
871 627033 HttpResponsePtr resp;
872
7/10
✓ Branch 0 taken 627033 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 325305 times.
✓ Branch 3 taken 301728 times.
✓ Branch 4 taken 325305 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 325305 times.
✓ Branch 8 taken 301728 times.
✓ Branch 9 taken 325305 times.
627033 if (req->method() == drogon::Get || req->isHead())
873
1/2
✓ Branch 0 taken 301728 times.
✗ Branch 1 not taken.
301728 resp = PixelOverlayManager::INSTANCE.render_GET(req);
874
3/4
✓ Branch 0 taken 325305 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 32 times.
✓ Branch 3 taken 325273 times.
325305 else if (req->method() == drogon::Post)
875
2/2
✓ Branch 0 taken 31 times.
✓ Branch 1 taken 1 times.
32 resp = PixelOverlayManager::INSTANCE.render_POST(req);
876
2/4
✓ Branch 0 taken 325273 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 325273 times.
✗ Branch 3 not taken.
325273 else if (req->method() == drogon::Put)
877
2/2
✓ Branch 0 taken 325232 times.
✓ Branch 1 taken 41 times.
325273 resp = PixelOverlayManager::INSTANCE.render_PUT(req);
878 else
879 ✗ resp = makeStringResponse("Method Not Allowed", 405);
880
1/2
✓ Branch 0 taken 626991 times.
✗ Branch 1 not taken.
626991 callback(resp);
881 627033 };
882
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.
11310 app.registerHandler("/models", copyHandler(handleModels), {drogon::Get, drogon::Post, drogon::Put, drogon::Head});
883
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.
11310 app.registerHandlerViaRegex("/models/.*", copyHandler(handleModels), {drogon::Get, drogon::Post, drogon::Put, drogon::Head});
884 // No bare "/overlays" registration: it was never a documented,
885 // intentional endpoint (checked the full doc history -- the pre-FPP10
886 // endpoints.json help page only ever listed "overlays/models" and
887 // "overlays/model/:ModelName", never bare "overlays"), Apache has never
888 // proxied it externally, and grep finds zero internal callers anywhere
889 // in the codebase. Only the real sub-paths are registered.
890
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.
11310 app.registerHandlerViaRegex("/overlays/.*", copyHandler(handleModels), {drogon::Get, drogon::Post, drogon::Put, drogon::Head});
891
892 // CommandManager (/command/*, /commands/*, /commandPresets/*)
893 190654 auto handleCommands = [](const HttpRequestPtr& req,
894 std::function<void(const HttpResponsePtr&)>&& callback) {
895 190654 HttpResponsePtr resp;
896
7/10
✓ Branch 0 taken 190654 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 190279 times.
✓ Branch 3 taken 375 times.
✓ Branch 4 taken 190279 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 190279 times.
✓ Branch 8 taken 375 times.
✓ Branch 9 taken 190279 times.
190654 if (req->method() == drogon::Get || req->isHead())
897
1/2
✓ Branch 0 taken 375 times.
✗ Branch 1 not taken.
375 resp = CommandManager::INSTANCE.render_GET(req);
898
2/4
✓ Branch 0 taken 190279 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 190279 times.
✗ Branch 3 not taken.
190279 else if (req->method() == drogon::Post)
899
2/2
✓ Branch 0 taken 190260 times.
✓ Branch 1 taken 19 times.
190279 resp = CommandManager::INSTANCE.render_POST(req);
900 else
901 ✗ resp = makeStringResponse("Method Not Allowed", 405);
902
1/2
✓ Branch 0 taken 190635 times.
✗ Branch 1 not taken.
190635 callback(resp);
903 190654 };
904 // Bare /command has no content of its own -- it 404s from CommandManager
905 // just like a bad sub-path would (confirmed by hitting it directly against
906 // fppd:32322). Registered anyway because /command/* needs a parent match;
907 // intentionally left undocumented since there's nothing real to document.
908
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.
11310 app.registerHandler("/command", copyHandler(handleCommands), {drogon::Get, drogon::Post, drogon::Head});
909
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.
11310 app.registerHandlerViaRegex("/command/.*", copyHandler(handleCommands), {drogon::Get, drogon::Post, drogon::Head});
910 /**
911 * List all defined commands.
912 *
913 * @route GET /api/commands
914 * @response 200 Array of commands.
915 */
916 // render_POST() only acts when the path's first segment is literally
917 // "command" (POST /api/command/{name}, already documented there) --
918 // bare POST here, and POST /api/commandPresets below, both 404
919 // "Not Found". Confirmed live.
920
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.
11310 app.registerHandler("/commands", copyHandler(handleCommands), {drogon::Get, drogon::Post, drogon::Head});
921
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.
11310 app.registerHandlerViaRegex("/commands/.*", copyHandler(handleCommands), {drogon::Get, drogon::Post, drogon::Head});
922 /**
923 * List all defined command presets.
924 *
925 * @route GET /api/commandPresets
926 * @response 200 Array of command presets.
927 */
928
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.
11310 app.registerHandler("/commandPresets", copyHandler(handleCommands), {drogon::Get, drogon::Post, drogon::Head});
929
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.
11310 app.registerHandlerViaRegex("/commandPresets/.*", copyHandler(handleCommands), {drogon::Get, drogon::Post, drogon::Head});
930
931 // GPIOManager (/gpio/*)
932 867 auto handleGpio = [](const HttpRequestPtr& req,
933 std::function<void(const HttpResponsePtr&)>&& callback) {
934 867 HttpResponsePtr resp;
935
7/10
✓ Branch 0 taken 867 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 521 times.
✓ Branch 3 taken 346 times.
✓ Branch 4 taken 521 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 521 times.
✓ Branch 8 taken 346 times.
✓ Branch 9 taken 521 times.
867 if (req->method() == drogon::Get || req->isHead())
936
1/2
✓ Branch 0 taken 346 times.
✗ Branch 1 not taken.
346 resp = GPIOManager::INSTANCE.render_GET(req);
937
2/4
✓ Branch 0 taken 521 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 521 times.
✗ Branch 3 not taken.
521 else if (req->method() == drogon::Post)
938
1/2
✓ Branch 0 taken 521 times.
✗ Branch 1 not taken.
521 resp = GPIOManager::INSTANCE.render_POST(req);
939 else
940 ✗ resp = makeStringResponse("Method Not Allowed", 405);
941
1/2
✓ Branch 0 taken 867 times.
✗ Branch 1 not taken.
867 callback(resp);
942 867 };
943 /**
944 * List all GPIO pins and their current state/configuration.
945 *
946 * @route GET /api/gpio
947 * @response 200 Array of GPIO pins.
948 */
949 // render_POST() only acts on POST /api/gpio/{pin} (already documented
950 // there); bare POST here 404s "Not Found". Confirmed live.
951
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.
11310 app.registerHandler("/gpio", copyHandler(handleGpio), {drogon::Get, drogon::Post, drogon::Head});
952
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.
11310 app.registerHandlerViaRegex("/gpio/.*", copyHandler(handleGpio), {drogon::Get, drogon::Post, drogon::Head});
953
954 // Variables (/variables/*, reachable externally as /api/variables/* via the
955 // Apache proxy whitelist in etc/apache2.site — same pattern as /gpio above)
956 27933 auto handleVariables = [](const HttpRequestPtr& req,
957 std::function<void(const HttpResponsePtr&)>&& callback) {
958 27933 HttpResponsePtr resp;
959
7/10
✓ Branch 0 taken 27933 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 540 times.
✓ Branch 3 taken 27393 times.
✓ Branch 4 taken 540 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 540 times.
✓ Branch 8 taken 27393 times.
✓ Branch 9 taken 540 times.
27933 if (req->method() == drogon::Get || req->isHead())
960
1/2
✓ Branch 0 taken 27393 times.
✗ Branch 1 not taken.
27393 resp = Variables::INSTANCE.render_GET(req);
961
3/4
✓ Branch 0 taken 540 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 270 times.
✓ Branch 3 taken 270 times.
540 else if (req->method() == drogon::Post)
962
1/2
✓ Branch 0 taken 270 times.
✗ Branch 1 not taken.
270 resp = Variables::INSTANCE.render_POST(req);
963
2/4
✓ Branch 0 taken 270 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 270 times.
✗ Branch 3 not taken.
270 else if (req->method() == drogon::Delete)
964
1/2
✓ Branch 0 taken 270 times.
✗ Branch 1 not taken.
270 resp = Variables::INSTANCE.render_DELETE(req);
965 else
966 ✗ resp = makeStringResponse("Method Not Allowed", 405);
967
1/2
✓ Branch 0 taken 27933 times.
✗ Branch 1 not taken.
27933 callback(resp);
968 27933 };
969 /**
970 * List all variables and their current values.
971 *
972 * @route GET /api/variables
973 * @response 200 Array of variables.
974 */
975 // Variables::render_POST()/render_DELETE() both require a name segment
976 // (POST /variables/{name}, DELETE /variables/{name} -- already
977 // documented there); bare POST/DELETE 400 "Bad Request". Confirmed live.
978
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.
11310 app.registerHandler("/variables", copyHandler(handleVariables), { drogon::Get, drogon::Post, drogon::Delete, drogon::Head });
979
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.
11310 app.registerHandlerViaRegex("/variables/.*", copyHandler(handleVariables), { drogon::Get, drogon::Post, drogon::Delete, drogon::Head });
980
981 // Player (/player/*)
982 39566 auto handlePlayer = [](const HttpRequestPtr& req,
983 std::function<void(const HttpResponsePtr&)>&& callback) {
984 39566 HttpResponsePtr resp;
985
3/10
✓ Branch 0 taken 39566 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 39566 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✓ Branch 8 taken 39566 times.
✗ Branch 9 not taken.
39566 if (req->method() == drogon::Get || req->isHead())
986
1/2
✓ Branch 0 taken 39566 times.
✗ Branch 1 not taken.
39566 resp = Player::INSTANCE.render_GET(req);
987 ✗ else if (req->method() == drogon::Post)
988 ✗ resp = Player::INSTANCE.render_POST(req);
989 ✗ else if (req->method() == drogon::Put)
990 ✗ resp = Player::INSTANCE.render_PUT(req);
991 else
992 ✗ resp = makeStringResponse("Method Not Allowed", 405);
993
1/2
✓ Branch 0 taken 39566 times.
✗ Branch 1 not taken.
39566 callback(resp);
994 39566 };
995 /**
996 * Get the current player state (playing/stopped, volume, current media, etc.).
997 *
998 * @route GET /api/player
999 * @response 200 Current player state.
1000 */
1001 // Player::render_POST()/render_PUT() are both unconditional
1002 // `return makeStringResponse("Not Found", 404, ...)` -- registered for
1003 // completeness/future use but dead code today. Confirmed live. Do not
1004 // document POST/PUT here without checking Player.cpp again.
1005
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.
11310 app.registerHandler("/player", copyHandler(handlePlayer), {drogon::Get, drogon::Post, drogon::Put, drogon::Head});
1006
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.
11310 app.registerHandlerViaRegex("/player/.*", copyHandler(handlePlayer), {drogon::Get, drogon::Post, drogon::Put, drogon::Head});
1007
1008 // Video input preview (/videoinput/preview?id=N)
1009 //
1010 // Returns a single JPEG frame tapped off a running video input source's
1011 // intervideo channel, so the config page can show the operator what the
1012 // capture device is actually producing. Tapping the channel (rather than
1013 // opening /dev/videoN again) means the preview works while the source is
1014 // live -- UVC cameras only allow a single opener.
1015 43 auto handleVideoInputPreview = [](const HttpRequestPtr& req,
1016 std::function<void(const HttpResponsePtr&)>&& callback) {
1017 43 int id = 0;
1018
3/6
✓ Branch 0 taken 43 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 43 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 43 times.
✗ Branch 5 not taken.
86 auto idStr = req->getParameter("id");
1019
1/2
✓ Branch 0 taken 43 times.
✗ Branch 1 not taken.
43 if (!idStr.empty()) {
1020 43 id = std::atoi(idStr.c_str());
1021 }
1022 43 int width = 320;
1023
3/6
✓ Branch 0 taken 43 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 43 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 43 times.
✗ Branch 5 not taken.
86 auto wStr = req->getParameter("width");
1024
1/2
✓ Branch 0 taken 43 times.
✗ Branch 1 not taken.
43 if (!wStr.empty()) {
1025 43 width = std::atoi(wStr.c_str());
1026 }
1027
1028 43 std::vector<uint8_t> jpeg;
1029
7/10
✓ Branch 0 taken 43 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 43 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 43 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 26 times.
✓ Branch 7 taken 17 times.
✓ Branch 8 taken 26 times.
✓ Branch 9 taken 17 times.
43 if (id <= 0 || !VideoInputManager::Instance().GrabSnapshotJPEG(id, width, 2000, jpeg)) {
1030
3/6
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 26 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 26 times.
✗ Branch 5 not taken.
78 auto resp = makeStringResponse("No frame available", 503);
1031
3/6
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 26 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 26 times.
✗ Branch 5 not taken.
104 resp->addHeader("Cache-Control", "no-store");
1032
1/2
✓ Branch 0 taken 26 times.
✗ Branch 1 not taken.
26 callback(resp);
1033 26 return;
1034 26 }
1035
1036
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 auto resp = drogon::HttpResponse::newHttpResponse();
1037
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 resp->setStatusCode(drogon::k200OK);
1038
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 resp->setContentTypeCode(drogon::CT_IMAGE_JPG);
1039
2/4
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
34 resp->setBody(std::string(reinterpret_cast<const char*>(jpeg.data()), jpeg.size()));
1040
3/6
✓ 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.
68 resp->addHeader("Cache-Control", "no-store");
1041
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 callback(resp);
1042
6/6
✓ Branch 0 taken 17 times.
✓ Branch 1 taken 26 times.
✓ Branch 2 taken 17 times.
✓ Branch 3 taken 26 times.
✓ Branch 4 taken 17 times.
✓ Branch 5 taken 26 times.
95 };
1043 /**
1044 * Get a JPEG snapshot of a running video input source, for UI preview.
1045 *
1046 * @route GET /api/videoinput/preview
1047 * @response 200 JPEG image of the most recent frame.
1048 * @response 503 Source is not running or produced no frame.
1049 */
1050
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.
9425 app.registerHandler("/videoinput/preview", copyHandler(handleVideoInputPreview), {drogon::Get, drogon::Head});
1051
1052 // Let plugins register their own routes. These live in FPP's plugin
1053 // route registry, reached through drogon's default handler, so plugins
1054 // loaded after run() can add routes too.
1055
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 FPPPlugins::installPluginApiRouter();
1056
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 PluginManager::INSTANCE.registerApis();
1057
1058 // Configure and start drogon in a separate thread (since app().run() blocks).
1059 // Bind both v4 and v6 loopback so Apache's reverse-proxy works no
1060 // matter how libc resolves "localhost" (modern glibc returns ::1
1061 // first per RFC 6724).
1062
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.
13035 app.addListener(FPP_BIND_ADDRESS, FPP_HTTP_PORT);
1063
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.
13035 app.addListener("::1", FPP_HTTP_PORT);
1064
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 app.setThreadNum(10);
1065
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 app.disableSession();
1066
1067 // Drogon's defaults are a 1MB max request body and a 64KB in-memory body,
1068 // above which it spools the body to a temp file. Both are too small for
1069 // FPP. The bulk pixel endpoints (PUT /api/overlays/model/{m}/data) send a
1070 // whole frame per request -- a 1024x1024 RGB model is 3MB, well past the
1071 // 1MB cap -- and spooling every frame of a repeating client to the SD card
1072 // would make that endpoint slower than the per-pixel API it replaces.
1073 // POST /api/models (model-overlays.json) was silently capped at 1MB too.
1074 //
1075 // The in-memory limit is what bounds worst-case RAM: setThreadNum(10) means
1076 // up to 10 concurrent bodies held in memory, so 4MB each is ~40MB worst
1077 // case -- affordable even on a 480MB BeagleBone. Requests larger than that
1078 // still work, they just go through a temp file.
1079
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 app.setClientMaxBodySize(32 * 1024 * 1024);
1080
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 app.setClientMaxMemoryBodySize(4 * 1024 * 1024);
1081
1082 // A sync advice runs before any router, for plain requests and WebSocket
1083 // upgrades alike; see QuiesceHttpServer().
1084
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 app.registerSyncAdvice([](const drogon::HttpRequestPtr&) -> drogon::HttpResponsePtr {
1085
2/2
✓ Branch 0 taken 1111811 times.
✓ Branch 1 taken 1 times.
1111812 if (!s_httpQuiescing) {
1086 1111811 return nullptr;
1087 }
1088
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 auto resp = drogon::HttpResponse::newHttpResponse();
1089
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 resp->setStatusCode(drogon::k503ServiceUnavailable);
1090
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 resp->setCloseConnection(true);
1091 1 return resp;
1092 1 });
1093
1094 // SIGINT and SIGTERM are FPP's, not drogon's. Drogon's handlers only stop
1095 // drogon's own threads, leaving the main loop spinning, and they log (so
1096 // allocate) and then run any callback given to them inside the signal
1097 // handler: a signal landing in malloc could deadlock the shutdown. This
1098 // handler only clears runMainFPPDLoop, a lock-free atomic. The main loop
1099 // sees it and runs FPP's normal shutdown, which stops drogon from
1100 // ~APIServer().
1101
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 app.disableSigtermHandling();
1102 1885 struct sigaction stopAct = {};
1103 3741 stopAct.sa_handler = [](int) { runMainFPPDLoop = 0; };
1104 1885 sigemptyset(&stopAct.sa_mask);
1105 1885 sigaction(SIGINT, &stopAct, nullptr);
1106 1885 sigaction(SIGTERM, &stopAct, nullptr);
1107
1108 // Register the /fppdws status push endpoint and its 1s change-poll timer.
1109 // The explicit call also keeps StatusWebSocket.o's controller
1110 // self-registration from being dropped by the linker.
1111
1/2
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
1885 StatusWebSocketInit();
1112
1113
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 1885 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
1885 m_serverThread = new std::thread([]() {
1114 1885 drogon::app().run();
1115
2/4
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1885 times.
✗ Branch 3 not taken.
1885 });
1116 // Do NOT detach: ~APIServer() joins this thread after quit() so Drogon is
1117 // fully stopped before PluginManager::Cleanup() calls dlclose() on plugin
1118 // libraries. Plugin route handlers are lambdas with code in those libraries;
1119 // Drogon IO threads running after dlclose() -> SEGV_MAPERR on the vtable.
1120 1885 }
1121
1122 /*
1123 *
1124 */
1125 223046 void LogRequest(const HttpRequestPtr& req) {
1126 // LOG_EXCESSIVE, not LOG_DEBUG: /fppd/status is polled about once a second
1127 // by fppoled, the web UI and anything else watching the player, so at debug
1128 // these three lines are a steady 3/sec. They never reach fppd.log at the
1129 // default level, but the crash-time log ring keeps everything <= LOG_DEBUG
1130 // (CrashLogRingWillCapture), and 256 slots of status polling is all a crash
1131 // report was left with - roughly 85 seconds of nothing. See log.cpp.
1132
4/8
✓ Branch 0 taken 223046 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 223046 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 221325 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 221325 times.
✗ Branch 7 not taken.
223046 LogExcess(VB_HTTP, "API Req: %s%s from %s\n", req->path().c_str(),
1133 req->query().c_str(), getEffectiveClientIP(req).c_str());
1134 223046 }
1135
1136 /*
1137 *
1138 */
1139 222698 void LogResponse(const HttpRequestPtr& req, int responseCode, const std::string& content) {
1140
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 222691 times.
222698 if (WillLog(LOG_EXCESSIVE, VB_HTTP)) {
1141 7 LogExcess(VB_HTTP, "API Res: %s%s %d %s\n",
1142 req->path().c_str(),
1143 req->query().c_str(),
1144 responseCode,
1145 content.c_str());
1146 }
1147 222698 }
1148
1149 1885 PlayerResource::PlayerResource() {
1150 #ifdef PLATFORM_PI
1151 #define DEVICE_FILE_NAME "/dev/vcio"
1152 #define MAJOR_NUM 100
1153 #define IOCTL_MBOX_PROPERTY _IOWR(MAJOR_NUM, 0, char*)
1154 #define MAX_STRING 1024
1155 #define GET_GENCMD_RESULT 0x00030080
1156 54 piPowerFile = open(DEVICE_FILE_NAME, 0);
1157 #else
1158 1831 piPowerFile = -1;
1159 #endif
1160 1885 }
1161 1867 PlayerResource::~PlayerResource() {
1162
2/2
✓ Branch 0 taken 54 times.
✓ Branch 1 taken 1813 times.
1867 if (piPowerFile > 0) {
1163 54 close(piPowerFile);
1164 }
1165 1867 }
1166
1167 /*
1168 *
1169 */
1170 // --------------------------------------------------------------------------
1171 // OpenAPI docs for the /fppd/* GET endpoints handled below.
1172 // These @route docblocks are parsed by www/api/tools/generate_openapi.py; the
1173 // fppd daemon serves them on port 32322 and Apache proxies them under /api/*.
1174 // --------------------------------------------------------------------------
1175
1176 /**
1177 * List the effects currently running on the player.
1178 *
1179 * @route GET /api/fppd/effects
1180 * @response 200 Object with a `runningEffects` array.
1181 */
1182
1183 /**
1184 * Get the current fppd logging configuration (log level and enabled channels).
1185 *
1186 * @route GET /api/fppd/log
1187 * @response 200 Current log settings.
1188 */
1189
1190 /**
1191 * Get the full current player status (playlist, sequence, time, mode, etc.).
1192 *
1193 * @route GET /api/fppd/status
1194 * @response 200 Current player status JSON.
1195 */
1196
1197 /**
1198 * List the messages for all currently active warnings.
1199 *
1200 * @route GET /api/fppd/warnings
1201 * @response 200 Array of warning message strings.
1202 * ```json
1203 * ["Low disk space", "No network connection"]
1204 * ```
1205 */
1206
1207 /**
1208 * List all currently active warnings as full objects (message plus metadata).
1209 *
1210 * @route GET /api/fppd/warnings_full
1211 * @response 200 Array of warning objects.
1212 */
1213
1214 /**
1215 * Get the number of E1.31/sACN bytes received per universe.
1216 *
1217 * @route GET /api/fppd/e131stats
1218 * @response 200 E1.31 receive statistics.
1219 */
1220
1221 /**
1222 * List the MultiSync systems discovered on the network.
1223 *
1224 * @route GET /api/fppd/multiSyncSystems
1225 * @param int localOnly Set to 1 to return only the local system.
1226 * @response 200 MultiSync systems.
1227 */
1228
1229 /**
1230 * Get MultiSync packet statistics.
1231 *
1232 * @route GET /api/fppd/multiSyncStats
1233 * @param int reset Set to 1 to reset the statistics after reading them.
1234 * @response 200 MultiSync statistics.
1235 */
1236
1237 /**
1238 * List the playlists that are currently running.
1239 *
1240 * @route GET /api/fppd/playlists
1241 * @response 200 Currently running playlists.
1242 */
1243
1244 /**
1245 * Get the last-modified time of the running playlist file.
1246 *
1247 * @route GET /api/fppd/playlist/filetime
1248 * @response 200 Playlist file time.
1249 */
1250
1251 /**
1252 * Get the configuration of the running playlist.
1253 *
1254 * @route GET /api/fppd/playlist/config
1255 * @response 200 Playlist configuration.
1256 */
1257
1258 /**
1259 * Get the currently loaded schedule.
1260 *
1261 * @route GET /api/fppd/schedule
1262 * @response 200 Object with a `schedule` member.
1263 */
1264
1265 /**
1266 * Expand the schedule over an arbitrary date range.
1267 *
1268 * Unlike `/api/fppd/schedule`, which reports only the rolling window fppd has
1269 * actually scheduled out, this expands the configured schedule rules across any
1270 * requested range so past and future dates can be previewed. Occurrences are
1271 * advisory - they describe what the schedule says should happen, not what fppd
1272 * has committed to running.
1273 *
1274 * @route GET /api/fppd/schedule/range
1275 * @param int start Range start as epoch seconds.
1276 * @param int end Range end as epoch seconds. Must be after `start` and no more than 420 days later.
1277 * @param boolean includeDisabled Set to `1` to also expand disabled schedule entries.
1278 * @param boolean summary Set to `1` to collapse to one item per schedule entry per day, each carrying a `count` of that day's occurrences. Intended for month and other coarse views, where a repeating entry would otherwise return thousands of items.
1279 * @response 200 Object with a `schedule` member containing `entries`, `items`, `rangeStart` and `rangeEnd`.
1280 * @response 400 Missing, malformed, or excessively large range.
1281 */
1282
1283 /**
1284 * Get FPP version information.
1285 *
1286 * @route GET /api/fppd/version
1287 * @response 200 Version details.
1288 * ```json
1289 * {"version": "9.0", "majorVersion": 9, "minorVersion": 0, "branch": "master", "fppdAPI": 4, "Status": "OK"}
1290 * ```
1291 */
1292
1293 /**
1294 * Get (or set) the master output volume.
1295 *
1296 * @route GET /api/fppd/volume
1297 * @param int set New volume (0-100) to apply before returning.
1298 * @param boolean simple Return the volume as a bare text/plain integer instead of JSON.
1299 * @response 200 Object with a `volume` member (0-100).
1300 */
1301
1302 /**
1303 * Get the list of running sequences.
1304 *
1305 * @route GET /api/fppd/sequence
1306 * @response 200 Running sequences.
1307 */
1308
1309 /**
1310 * Dump the cached MQTT messages.
1311 *
1312 * @route GET /api/fppd/mqtt/cache
1313 * @response 200 Object keyed by topic, each value the topic's last cached message as a plain string.
1314 * Per-topic last-updated timestamps are exposed separately via
1315 * `GET /api/variables?mqtt=true` (`mqtt-<topic>` entries), not on this route.
1316 * @response 400 MQTT is not initialized.
1317 */
1318 216452 HttpResponsePtr PlayerResource::render_GET(const HttpRequestPtr& req) {
1319
1/2
✓ Branch 0 taken 216452 times.
✗ Branch 1 not taken.
216452 LogRequest(req);
1320
1321
1/2
✓ Branch 0 taken 216452 times.
✗ Branch 1 not taken.
216452 Json::Value result;
1322
1/2
✓ Branch 0 taken 216452 times.
✗ Branch 1 not taken.
216452 std::string resultStr = "";
1323
2/4
✓ Branch 0 taken 216452 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 216452 times.
✗ Branch 3 not taken.
216452 std::string url(req->path());
1324
1325
4/8
✓ Branch 0 taken 216452 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 216452 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 216452 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 216452 times.
865808 if (!replaceStart(url, "/fppd/", ""))
1326 ✗ replaceStart(url, "/fppd", "");
1327
1328 // Strip trailing slash to match endpoint definitions
1329
4/6
✓ Branch 0 taken 216452 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 216452 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 216451 times.
432904 if (endsWith(url, "/"))
1330
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 url = url.substr(0, url.length() - 1);
1331
1332 // LOG_EXCESSIVE for the same reason as LogRequest() above.
1333
2/4
✓ Branch 0 taken 216452 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 216452 times.
✗ Branch 3 not taken.
216452 LogExcess(VB_HTTP, "URL: %s %s\n", url.c_str(), req->query().c_str());
1334
1335 // Keep IF statement in alphabetical order
1336
3/4
✓ Branch 0 taken 216452 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 197 times.
✓ Branch 3 taken 216255 times.
216452 if (url == "effects") {
1337
1/2
✓ Branch 0 taken 197 times.
✗ Branch 1 not taken.
197 GetRunningEffects(result);
1338
3/4
✓ Branch 0 taken 216255 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✓ Branch 3 taken 216239 times.
216255 } else if (url == "log") {
1339
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 GetLogSettings(result);
1340
3/4
✓ Branch 0 taken 216239 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 137329 times.
✓ Branch 3 taken 78910 times.
216239 } else if (url == "status") {
1341
1/2
✓ Branch 0 taken 137329 times.
✗ Branch 1 not taken.
137329 GetCurrentStatus(result);
1342
3/4
✓ Branch 0 taken 78910 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 96 times.
✓ Branch 3 taken 78814 times.
78910 } else if (url == "warnings") {
1343
1/2
✓ Branch 0 taken 96 times.
✗ Branch 1 not taken.
96 result = Json::Value(Json::ValueType::arrayValue);
1344
3/4
✓ Branch 0 taken 96 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 83 times.
✓ Branch 3 taken 96 times.
179 for (auto& warn : WarningHolder::GetWarnings()) {
1345
2/4
✓ Branch 0 taken 83 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 83 times.
✗ Branch 3 not taken.
83 result.append(warn.message());
1346 96 }
1347
3/4
✓ Branch 0 taken 78814 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✓ Branch 3 taken 78797 times.
78814 } else if (url == "warnings_full") {
1348
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 result = Json::Value(Json::ValueType::arrayValue);
1349
3/4
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✓ Branch 3 taken 17 times.
33 for (auto& warn : WarningHolder::GetWarnings()) {
1350
2/4
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✗ Branch 3 not taken.
16 result.append(warn.toJsonValue());
1351 17 }
1352
3/4
✓ Branch 0 taken 78797 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 10325 times.
✓ Branch 3 taken 68472 times.
78797 } else if (url == "e131stats") {
1353
1/2
✓ Branch 0 taken 10325 times.
✗ Branch 1 not taken.
10325 GetE131BytesReceived(result);
1354
3/4
✓ Branch 0 taken 68472 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16307 times.
✓ Branch 3 taken 52165 times.
68472 } else if (url == "multiSyncSystems") {
1355 16307 bool localOnly = false;
1356
1357
4/8
✓ Branch 0 taken 16307 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16307 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 16307 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 16307 times.
32614 if (getRequestArg(req, "localOnly") == "1")
1358 ✗ localOnly = true;
1359
1360
1/2
✓ Branch 0 taken 16307 times.
✗ Branch 1 not taken.
16307 GetMultiSyncSystems(result, localOnly);
1361
3/4
✓ Branch 0 taken 52165 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 89 times.
✓ Branch 3 taken 52076 times.
52165 } else if (url == "multiSyncStats") {
1362 89 bool reset = false;
1363
1364
5/8
✓ Branch 0 taken 89 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 89 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 89 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✓ Branch 7 taken 87 times.
178 if (getRequestArg(req, "reset") == "1")
1365 2 reset = true;
1366
1367
1/2
✓ Branch 0 taken 89 times.
✗ Branch 1 not taken.
89 GetMultiSyncStats(result, reset);
1368
3/4
✓ Branch 0 taken 52076 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 52071 times.
52076 } else if (url == "playlists") {
1369
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 GetCurrentPlaylists(result);
1370
3/4
✓ Branch 0 taken 52071 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 52066 times.
52071 } else if (url == "playlist/filetime") {
1371
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 GetPlaylistFileTime(result);
1372
3/4
✓ Branch 0 taken 52066 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 52060 times.
52066 } else if (url == "playlist/config") {
1373
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 GetPlaylistConfig(result);
1374
3/4
✓ Branch 0 taken 52060 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 55 times.
✓ Branch 3 taken 52005 times.
52060 } else if (url == "recurringtasks") {
1375
1/2
✓ Branch 0 taken 55 times.
✗ Branch 1 not taken.
55 GetRecurringTasks(result);
1376
3/4
✓ Branch 0 taken 52005 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✓ Branch 3 taken 51989 times.
52005 } else if (url == "condition/preview") {
1377
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 GetConditionPreview(req, result);
1378
3/4
✓ Branch 0 taken 51989 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 36326 times.
✓ Branch 3 taken 15663 times.
51989 } else if (url == "schedule") {
1379
2/4
✓ Branch 0 taken 36326 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 36326 times.
✗ Branch 3 not taken.
36326 result["schedule"] = scheduler->GetSchedule();
1380
2/4
✓ Branch 0 taken 36326 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 36326 times.
✗ Branch 3 not taken.
72652 SetOKResult(result, "");
1381
3/4
✓ Branch 0 taken 15663 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 28 times.
✓ Branch 3 taken 15635 times.
15663 } else if (url == "schedule/range") {
1382 // Expanding the schedule costs time proportional to the range, so cap
1383 // what a single request can ask for rather than letting a stray query
1384 // hold the scheduler lock.
1385 28 constexpr int MAX_SCHEDULE_RANGE_DAYS = 420;
1386
1387
2/4
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 28 times.
✗ Branch 3 not taken.
56 time_t rangeStart = (time_t)std::atoll(getRequestArg(req, "start").c_str());
1388
2/4
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 28 times.
✗ Branch 3 not taken.
28 time_t rangeEnd = (time_t)std::atoll(getRequestArg(req, "end").c_str());
1389
1390
4/4
✓ Branch 0 taken 20 times.
✓ Branch 1 taken 8 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 19 times.
28 if ((rangeStart <= 0) || (rangeEnd <= rangeStart)) {
1391
2/4
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
18 SetErrorResult(result, 400, "start and end must be epoch seconds with end after start");
1392
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 18 times.
19 } else if ((rangeEnd - rangeStart) > ((time_t)MAX_SCHEDULE_RANGE_DAYS * 86400)) {
1393
3/6
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
1 SetErrorResult(result, 400, "range may not exceed " + std::to_string(MAX_SCHEDULE_RANGE_DAYS) + " days");
1394 } else {
1395
3/6
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 18 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 18 times.
✗ Branch 5 not taken.
36 bool includeDisabled = getRequestArg(req, "includeDisabled") == "1";
1396
3/6
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 18 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 18 times.
✗ Branch 5 not taken.
18 bool daySummary = getRequestArg(req, "summary") == "1";
1397
1398
2/4
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 18 times.
✗ Branch 3 not taken.
36 result["schedule"] = scheduler->GetScheduleRange(rangeStart, rangeEnd,
1399 18 includeDisabled, daySummary);
1400
2/4
✓ Branch 0 taken 18 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 18 times.
✗ Branch 3 not taken.
36 SetOKResult(result, "");
1401 }
1402
3/4
✓ Branch 0 taken 15635 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 15630 times.
15635 } else if (url == "version") {
1403
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.
5 result["version"] = getFPPVersion();
1404
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.
5 result["majorVersion"] = getFPPMajorVersion();
1405
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.
5 result["minorVersion"] = getFPPMinorVersion();
1406
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.
5 result["branch"] = getFPPBranch();
1407
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 result["fppdAPI"] = FPPD_API_VERSION;
1408
1409
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
10 SetOKResult(result, "");
1410
3/4
✓ Branch 0 taken 15630 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 52 times.
✓ Branch 3 taken 15578 times.
15630 } else if (url == "volume") {
1411
4/8
✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 52 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 52 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 52 times.
104 if (getRequestArg(req, "set") != "") {
1412 ✗ int i = std::atoi(getRequestArg(req, "set").c_str());
1413 ✗ setVolume(i);
1414 }
1415
5/8
✓ Branch 0 taken 52 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 52 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 52 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✓ Branch 7 taken 51 times.
104 if (getRequestArg(req, "simple") == "true") {
1416
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 std::string v = std::to_string(getVolume());
1417
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 return makeStringResponse(v, 200, "text/plain");
1418 1 }
1419
3/6
✓ Branch 0 taken 51 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 51 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 51 times.
✗ Branch 5 not taken.
51 result["volume"] = getVolume();
1420
2/4
✓ Branch 0 taken 51 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 51 times.
✗ Branch 3 not taken.
102 SetOKResult(result, "");
1421
3/4
✓ Branch 0 taken 15578 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15558 times.
✓ Branch 3 taken 20 times.
15578 } else if (url == "sequence") {
1422
1/2
✓ Branch 0 taken 15558 times.
✗ Branch 1 not taken.
15558 LogDebug(VB_HTTP, "API - Getting list of running sequences\n");
1423
2/4
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 20 times.
✗ Branch 3 not taken.
20 } else if (url == "mqtt/cache") {
1424
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 LogDebug(VB_HTTP, "API - Getting MQTT Cached data\n");
1425
2/2
✓ Branch 0 taken 15 times.
✓ Branch 1 taken 5 times.
20 if (mqtt) {
1426
2/4
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
15 result["respCode"] = 200;
1427
2/4
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
15 result["Status"] = "OK";
1428
1/2
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
15 mqtt->dumpMessageCache(result);
1429 } else {
1430
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 result["Status"] = "ERROR";
1431
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 result["respCode"] = 400;
1432
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 result["Message"] = "Mqtt not Initialized";
1433 }
1434 } else {
1435 // url is an unvalidated caller-supplied path - truncate so an unknown
1436 // endpoint can't let a caller choose how many bytes of fppd.log each
1437 // request costs (bounded only by Apache's request-line limit).
1438 ✗ LogErr(VB_HTTP, "API - Error unknown GET request: %s\n", TruncateForLog(url).c_str());
1439
1440 ✗ result["Status"] = "ERROR";
1441 ✗ result["respCode"] = 404;
1442 ✗ result["Message"] = std::string("endpoint fppd/") + url + " does not exist";
1443 }
1444
1445 216451 int responseCode = 200;
1446
6/8
✓ Branch 0 taken 216451 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 216451 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 216338 times.
✓ Branch 5 taken 113 times.
✓ Branch 6 taken 216338 times.
✓ Branch 7 taken 113 times.
216451 if (resultStr.empty() && !result.isArray()) {
1447
3/4
✓ Branch 0 taken 216338 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15558 times.
✓ Branch 3 taken 200780 times.
216338 if (result.empty()) {
1448
2/4
✓ Branch 0 taken 15558 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15558 times.
✗ Branch 3 not taken.
15558 result["Status"] = "ERROR";
1449
2/4
✓ Branch 0 taken 15558 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15558 times.
✗ Branch 3 not taken.
15558 result["respCode"] = 400;
1450
2/4
✓ Branch 0 taken 15558 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15558 times.
✗ Branch 3 not taken.
15558 result["Message"] = "GET endpoint helper did not set result JSON";
1451
3/4
✓ Branch 0 taken 200780 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 63451 times.
✓ Branch 3 taken 137329 times.
200780 } else if (result.isMember("respCode")) {
1452
2/4
✓ Branch 0 taken 63451 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 63451 times.
✗ Branch 3 not taken.
63451 responseCode = result["respCode"].asInt();
1453 }
1454 }
1455
1/2
✓ Branch 0 taken 216451 times.
✗ Branch 1 not taken.
216451 if (resultStr.empty()) {
1456
2/4
✓ Branch 0 taken 216451 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 216451 times.
✗ Branch 3 not taken.
432902 resultStr = SaveJsonToString(result);
1457 }
1458
1/2
✓ Branch 0 taken 216451 times.
✗ Branch 1 not taken.
216451 LogResponse(req, responseCode, resultStr);
1459
1460
2/4
✓ Branch 0 taken 216451 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 216451 times.
✗ Branch 3 not taken.
432902 return makeStringResponse(resultStr, responseCode, "application/json");
1461 216452 }
1462
1463 /*
1464 *
1465 */
1466 // --------------------------------------------------------------------------
1467 // OpenAPI docs for the /fppd/* POST endpoints handled below.
1468 // --------------------------------------------------------------------------
1469
1470 /**
1471 * Start (or update) a named overlay effect on the player.
1472 *
1473 * @route POST /api/fppd/effects/{name}
1474 * @response 200 Effect started.
1475 */
1476
1477 /**
1478 * Re-read the Falcon hardware (e.g. cape/receiver) configuration.
1479 *
1480 * @route POST /api/fppd/falcon/hardware
1481 * @response 200 Hardware refreshed.
1482 */
1483
1484 /**
1485 * Set an external GPIO input state.
1486 *
1487 * @route POST /api/fppd/gpio/ext
1488 * @response 200 GPIO state updated.
1489 */
1490
1491 /**
1492 * Set logging levels.
1493 *
1494 * There is no separate channel parameter: the {level} path segment carries both
1495 * the level and the optional channel targeting. A bare level name - one of
1496 * error, warn, info, debug, or excess - sets that level globally for every log
1497 * channel. To target specific channels instead, use level:channel,channel and
1498 * separate multiple groups with a semicolon, e.g. debug:Schedule,Player;info:Sync.
1499 * Channel names are case-sensitive and match those returned by GET /api/fppd/log
1500 * (for example Command, Control, HTTP, Schedule, Sync).
1501 *
1502 * @route POST /api/fppd/log/level/{level}
1503 * @pathparam level enum:error,warn,info,debug,excess example:debug Log level to apply globally, or a level:channel[,channel] targeting expression.
1504 * @response 200 Log level updated.
1505 * @response 400 Invalid or unrecognized log level.
1506 */
1507
1508 /**
1509 * Apply a new channel-output configuration.
1510 *
1511 * @route POST /api/fppd/outputs
1512 * @response 200 Outputs updated.
1513 */
1514
1515 /**
1516 * Remap channel outputs.
1517 *
1518 * @route POST /api/fppd/outputs/remap
1519 * @response 200 Outputs remapped.
1520 */
1521
1522 /**
1523 * Replace the active schedule.
1524 *
1525 * @route POST /api/fppd/schedule
1526 * @response 200 Schedule updated.
1527 */
1528
1529 /**
1530 * Set the master output volume (0-100).
1531 *
1532 * @route POST /api/fppd/volume/{volume}
1533 * @response 200 Object with the new `volume`.
1534 */
1535
1536 /**
1537 * Shut down the fppd daemon.
1538 *
1539 * @route POST /api/fppd/shutdown
1540 * @response 200 fppd shutting down.
1541 */
1542 1218 HttpResponsePtr PlayerResource::render_POST(const HttpRequestPtr& req) {
1543
1/2
✓ Branch 0 taken 1218 times.
✗ Branch 1 not taken.
1218 LogRequest(req);
1544
1545
1/2
✓ Branch 0 taken 1218 times.
✗ Branch 1 not taken.
1218 Json::Value data;
1546
1/2
✓ Branch 0 taken 1218 times.
✗ Branch 1 not taken.
1218 Json::Value result;
1547
2/4
✓ Branch 0 taken 1218 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1218 times.
✗ Branch 3 not taken.
1218 std::string url(req->path());
1548
1549
4/8
✓ Branch 0 taken 1218 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1218 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1218 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 1218 times.
4872 if (!replaceStart(url, "/fppd/", ""))
1550 ✗ replaceStart(url, "/fppd", "");
1551
1552 // Strip trailing slash to match endpoint definitions
1553
4/6
✓ Branch 0 taken 1218 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1218 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 47 times.
✓ Branch 5 taken 1171 times.
2436 if (endsWith(url, "/"))
1554
1/2
✓ Branch 0 taken 47 times.
✗ Branch 1 not taken.
47 url = url.substr(0, url.length() - 1);
1555
1556
2/4
✓ Branch 0 taken 1218 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1218 times.
✗ Branch 3 not taken.
1218 LogDebug(VB_HTTP, "POST URL: %s %s\n", url.c_str(), req->query().c_str());
1557
1558
4/6
✓ Branch 0 taken 1218 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1218 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1037 times.
✓ Branch 5 taken 181 times.
1218 if (getRequestContent(req) != "") {
1559
5/6
✓ Branch 0 taken 1037 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 978 times.
✓ Branch 3 taken 59 times.
✓ Branch 4 taken 177 times.
✓ Branch 5 taken 801 times.
1096 if (!LoadJsonFromString(getRequestContent(req), data)) {
1560
1/2
✓ Branch 0 taken 177 times.
✗ Branch 1 not taken.
177 LogErr(VB_CHANNELOUT, "Error parsing POST content\n");
1561
3/6
✓ Branch 0 taken 177 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 177 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 177 times.
✗ Branch 5 not taken.
708 return makeStringResponse("Error parsing POST content", 400);
1562 }
1563 }
1564
1565 // Keep IF statement in alphabetical order
1566
5/8
✓ Branch 0 taken 982 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 982 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 982 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 187 times.
✓ Branch 7 taken 795 times.
3928 if (replaceStart(url, "effects/")) {
1567
2/2
✓ Branch 0 taken 102 times.
✓ Branch 1 taken 85 times.
187 PostEffects(url, data, result);
1568
4/8
✓ Branch 0 taken 795 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 795 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 795 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 795 times.
3180 } else if (replaceStart(url, "falcon/hardware")) {
1569 ✗ PostFalconHardware(result);
1570
5/8
✓ Branch 0 taken 795 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 795 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 795 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 11 times.
✓ Branch 7 taken 784 times.
3180 } else if (replaceStart(url, "gpio/ext")) {
1571
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 4 times.
11 PostGPIOExt(data, result);
1572
5/8
✓ Branch 0 taken 784 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 784 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 784 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 208 times.
✓ Branch 7 taken 576 times.
3136 } else if (replaceStart(url, "log/level/")) {
1573 // SetLogLevelComplex returns false when the level (or targeted channel)
1574 // is not recognized and nothing was changed. Report that back rather
1575 // than falsely claiming success.
1576
3/4
✓ Branch 0 taken 208 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 19 times.
✓ Branch 3 taken 189 times.
208 if (SetLogLevelComplex(url)) {
1577 // Persist the resulting per-channel levels to the settings file so
1578 // they survive a restart and stay in sync with the Logging settings
1579 // page (which reads/writes the LogLevel_* settings).
1580
2/2
✓ Branch 0 taken 304 times.
✓ Branch 1 taken 19 times.
323 for (FPPLoggerInstance* logger : FPPLogger::INSTANCE.allInstances()) {
1581
4/8
✓ Branch 0 taken 304 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 304 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 304 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 304 times.
✗ Branch 7 not taken.
304 setSetting("LogLevel_" + logger->name, LogLevelToString(FacilityLevel(*logger)), true);
1582 }
1583
1/2
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
19 WarningHolder::UpdateLogLevelWarnings();
1584
2/4
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 19 times.
✗ Branch 3 not taken.
38 SetOKResult(result, "Log Level Updated");
1585 } else {
1586
2/4
✓ Branch 0 taken 189 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 189 times.
✗ Branch 3 not taken.
189 SetErrorResult(result, 400, "Invalid or unrecognized log level: " + url);
1587 }
1588
3/4
✓ Branch 0 taken 576 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✓ Branch 3 taken 562 times.
576 } else if (url == "outputs") {
1589
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 5 times.
14 PostOutputs(data, result);
1590
3/4
✓ Branch 0 taken 562 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 11 times.
✓ Branch 3 taken 551 times.
562 } else if (url == "outputs/remap") {
1591
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 5 times.
11 PostOutputsRemap(data, result);
1592
3/4
✓ Branch 0 taken 551 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 32 times.
✓ Branch 3 taken 519 times.
551 } else if (url == "recurringtasks") {
1593
2/2
✓ Branch 0 taken 25 times.
✓ Branch 1 taken 7 times.
32 PostRecurringTasks(data, result);
1594
3/4
✓ Branch 0 taken 519 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 257 times.
✓ Branch 3 taken 262 times.
519 } else if (url == "schedule") {
1595
3/4
✓ Branch 0 taken 257 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 252 times.
✓ Branch 3 taken 5 times.
262 PostSchedule(data, result);
1596
2/2
✓ Branch 0 taken 228 times.
✓ Branch 1 taken 34 times.
262 } else if (url.find("volume/") == 0) {
1597 // A whole number, or nothing changes: atoi() read garbage ("x", a
1598 // number too big for an int) as 0 or -1, and that muted the show.
1599
1/2
✓ Branch 0 taken 228 times.
✗ Branch 1 not taken.
228 std::string v = url.substr(7);
1600 228 char* endp = nullptr;
1601 228 errno = 0;
1602 228 long volume = strtol(v.c_str(), &endp, 10);
1603 // the whole value must be digits: an embedded NUL ("%00") stops
1604 // strtol() as if the string had ended
1605
7/8
✓ Branch 0 taken 228 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 71 times.
✓ Branch 3 taken 157 times.
✓ Branch 4 taken 12 times.
✓ Branch 5 taken 59 times.
✓ Branch 6 taken 169 times.
✓ Branch 7 taken 59 times.
228 if (v.empty() || endp != v.c_str() + v.size() || errno == ERANGE) {
1606
2/4
✓ Branch 0 taken 169 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 169 times.
✗ Branch 3 not taken.
338 SetErrorResult(result, 400, "Volume must be a whole number, 0-100");
1607 } else {
1608
2/4
✓ Branch 0 taken 59 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 59 times.
✗ Branch 3 not taken.
59 setVolume((int)std::clamp(volume, 0L, 100L));
1609
3/6
✓ Branch 0 taken 59 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 59 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 59 times.
✗ Branch 5 not taken.
59 result["volume"] = getVolume();
1610
2/4
✓ Branch 0 taken 59 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 59 times.
✗ Branch 3 not taken.
118 SetOKResult(result, "Volume set");
1611 }
1612
2/4
✓ Branch 0 taken 34 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 34 times.
262 } else if (url == "shutdown") {
1613 ✗ SetOKResult(result, "Shutting down fppd");
1614 ✗ ShutdownFPPD();
1615 } else {
1616
2/4
✓ Branch 0 taken 34 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 34 times.
✗ Branch 3 not taken.
34 LogErr(VB_HTTP, "API - Error unknown POST request: %s\n", TruncateForLog(url).c_str());
1617
1618
2/4
✓ Branch 0 taken 34 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 34 times.
✗ Branch 3 not taken.
34 result["Status"] = "ERROR";
1619
2/4
✓ Branch 0 taken 34 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 34 times.
✗ Branch 3 not taken.
34 result["respCode"] = 404;
1620
5/10
✓ Branch 0 taken 34 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 34 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 34 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 34 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 34 times.
✗ Branch 9 not taken.
68 result["Message"] = std::string("endpoint fppd/") + url + " does not exist";
1621 }
1622
1623
3/4
✓ Branch 0 taken 871 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 867 times.
871 if (!result.isMember("Status")) {
1624
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 result["Status"] = "ERROR";
1625
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 result["respCode"] = 400;
1626
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 result["Message"] = "POST endpoint helper did not set result JSON";
1627 }
1628
1629
2/4
✓ Branch 0 taken 871 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 871 times.
✗ Branch 3 not taken.
871 std::string resultStr = SaveJsonToString(result);
1630
3/6
✓ Branch 0 taken 871 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 871 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 871 times.
✗ Branch 5 not taken.
871 LogResponse(req, result["respCode"].asInt(), resultStr);
1631
1632
5/10
✓ Branch 0 taken 871 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 871 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 871 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 871 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 871 times.
✗ Branch 9 not taken.
2613 return makeStringResponse(resultStr.c_str(), result["respCode"].asInt(), "application/json");
1633 1558 }
1634
1635 /*
1636 *
1637 */
1638 /**
1639 * Reset the E1.31/sACN receive statistics.
1640 *
1641 * @route DELETE /api/fppd/e131stats
1642 * @response 200 Statistics cleared.
1643 */
1644 5376 HttpResponsePtr PlayerResource::render_DELETE(const HttpRequestPtr& req) {
1645
1/2
✓ Branch 0 taken 5376 times.
✗ Branch 1 not taken.
5376 LogRequest(req);
1646
1647
1/2
✓ Branch 0 taken 5376 times.
✗ Branch 1 not taken.
5376 Json::Value result;
1648
2/4
✓ Branch 0 taken 5376 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5376 times.
✗ Branch 3 not taken.
5376 std::string url(req->path());
1649
1650
4/8
✓ Branch 0 taken 5376 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5376 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 5376 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 5376 times.
21504 if (!replaceStart(url, "/fppd/", ""))
1651 ✗ replaceStart(url, "/fppd", "");
1652
1653 // Strip trailing slash to match endpoint definitions
1654
4/6
✓ Branch 0 taken 5376 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5376 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 5375 times.
10752 if (endsWith(url, "/"))
1655
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 url = url.substr(0, url.length() - 1);
1656
1657
2/4
✓ Branch 0 taken 5376 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5376 times.
✗ Branch 3 not taken.
5376 LogDebug(VB_HTTP, "DELETE URL: %s %s\n", url.c_str(), req->query().c_str());
1658
1659 // Keep IF statement in alphabetical order
1660
2/4
✓ Branch 0 taken 5376 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5376 times.
✗ Branch 3 not taken.
5376 if (url == "e131stats") {
1661
1/2
✓ Branch 0 taken 5376 times.
✗ Branch 1 not taken.
5376 LogDebug(VB_HTTP, "Resetting E131 Statistics");
1662
1/2
✓ Branch 0 taken 5376 times.
✗ Branch 1 not taken.
5376 ResetBytesReceived();
1663
2/4
✓ Branch 0 taken 5376 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5376 times.
✗ Branch 3 not taken.
10752 SetOKResult(result, "Stats Cleared");
1664 } else {
1665 ✗ LogErr(VB_HTTP, "API - Error unknown DELETE request: %s\n", TruncateForLog(url).c_str());
1666
1667 ✗ result["Status"] = "ERROR";
1668 ✗ result["respCode"] = 404;
1669 ✗ result["Message"] = std::string("endpoint fppd/") + url + " does not exist";
1670 }
1671
1672
2/4
✓ Branch 0 taken 5376 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 5376 times.
5376 if (!result.isMember("Status")) {
1673 ✗ result["Status"] = "ERROR";
1674 ✗ result["respCode"] = 400;
1675 ✗ result["Message"] = "DELETE endpoint helper did not set result JSON";
1676 }
1677
1678
2/4
✓ Branch 0 taken 5376 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5376 times.
✗ Branch 3 not taken.
5376 std::string resultStr = SaveJsonToString(result);
1679
1680
3/6
✓ Branch 0 taken 5376 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5376 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 5376 times.
✗ Branch 5 not taken.
5376 LogResponse(req, result["respCode"].asInt(), resultStr);
1681
1682
5/10
✓ Branch 0 taken 5376 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5376 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 5376 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 5376 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 5376 times.
✗ Branch 9 not taken.
21504 return makeStringResponse(resultStr.c_str(), result["respCode"].asInt());
1683 5376 }
1684
1685 /*
1686 *
1687 */
1688 ✗ HttpResponsePtr PlayerResource::render_PUT(const HttpRequestPtr& req) {
1689 ✗ LogRequest(req);
1690
1691 ✗ Json::Value result;
1692 ✗ std::string url(req->path());
1693
1694 ✗ if (!replaceStart(url, "/fppd/", ""))
1695 ✗ replaceStart(url, "/fppd", "");
1696
1697 // Strip trailing slash to match endpoint definitions
1698 ✗ if (endsWith(url, "/"))
1699 ✗ url = url.substr(0, url.length() - 1);
1700
1701 ✗ LogDebug(VB_HTTP, "PUT URL: %s %s\n", url.c_str(), req->query().c_str());
1702
1703 // No PUT endpoints are currently implemented under /fppd/*.
1704 ✗ LogErr(VB_HTTP, "API - Error unknown PUT request: %s\n", TruncateForLog(url).c_str());
1705
1706 ✗ result["Status"] = "ERROR";
1707 ✗ result["respCode"] = 404;
1708 ✗ result["Message"] = std::string("endpoint fppd/") + url + " does not exist";
1709
1710 ✗ std::string resultStr = SaveJsonToString(result);
1711
1712 ✗ LogResponse(req, result["respCode"].asInt(), resultStr);
1713
1714 ✗ return makeStringResponse(resultStr.c_str(), result["respCode"].asInt());
1715 ✗ }
1716
1717 /*
1718 *
1719 */
1720 69160 void PlayerResource::SetOKResult(Json::Value& result, const std::string& msg) {
1721
2/4
✓ Branch 0 taken 69160 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 69160 times.
✗ Branch 3 not taken.
69160 result["Status"] = "OK";
1722
2/4
✓ Branch 0 taken 69160 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 69160 times.
✗ Branch 3 not taken.
69160 result["respCode"] = 200;
1723
2/4
✓ Branch 0 taken 69160 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 69160 times.
✗ Branch 3 not taken.
69160 result["Message"] = msg;
1724 69160 }
1725
1726 /*
1727 *
1728 */
1729 480 void PlayerResource::SetErrorResult(Json::Value& result, const int respCode, const std::string& msg) {
1730
2/4
✓ Branch 0 taken 480 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 480 times.
✗ Branch 3 not taken.
480 result["Status"] = "ERROR";
1731
2/4
✓ Branch 0 taken 480 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 480 times.
✗ Branch 3 not taken.
480 result["respCode"] = respCode;
1732
2/4
✓ Branch 0 taken 480 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 480 times.
✗ Branch 3 not taken.
480 result["Message"] = msg;
1733 480 }
1734
1735 /*
1736 *
1737 */
1738 197 void PlayerResource::GetRunningEffects(Json::Value& result) {
1739 197 LogDebug(VB_HTTP, "API - Getting list of running effects\n");
1740
2/4
✓ Branch 0 taken 197 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 197 times.
✗ Branch 3 not taken.
197 SetOKResult(result, "");
1741
2/4
✓ Branch 0 taken 197 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 197 times.
✗ Branch 3 not taken.
197 result["runningEffects"] = GetRunningEffectsJson();
1742 197 }
1743
1744 /*
1745 *
1746 */
1747 16 void PlayerResource::GetLogSettings(Json::Value& result) {
1748
2/4
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✗ Branch 3 not taken.
16 SetOKResult(result, "");
1749
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 Json::Value log;
1750
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 std::vector<FPPLoggerInstance*> allLoggers = FPPLogger::INSTANCE.allInstances();
1751 16 std::vector<FPPLoggerInstance*>::iterator it;
1752
1753
2/2
✓ Branch 0 taken 256 times.
✓ Branch 1 taken 16 times.
272 for (it = allLoggers.begin(); it != allLoggers.end(); it++) {
1754 256 FPPLoggerInstance* ptr = *it;
1755
4/8
✓ Branch 0 taken 256 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 256 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 256 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 256 times.
✗ Branch 7 not taken.
256 log[ptr->name] = LogLevelToString(FacilityLevel(*ptr));
1756 }
1757
1758
2/4
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✗ Branch 3 not taken.
16 result["log"] = log;
1759 16 }
1760
1761 /*
1762 *
1763 */
1764 137329 void PlayerResource::GetCurrentStatus(Json::Value& result) {
1765 // LOG_EXCESSIVE for the same reason as LogRequest() above.
1766 137329 LogExcess(VB_HTTP, "API - Getting fppd status\n");
1767 137329 GetCurrentFPPDStatus(result);
1768 137329 }
1769
1770 /*
1771 *
1772 */
1773 5 void PlayerResource::GetCurrentPlaylists(Json::Value& result) {
1774
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 LogDebug(VB_HTTP, "API - Getting current playlist\n");
1775
1776
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 Json::Value names(Json::arrayValue);
1777
1778
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 if (Player::INSTANCE.IsPlaying())
1779
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.
5 names.append(Player::INSTANCE.GetPlaylistName());
1780
1781
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 result["playlists"] = names;
1782
1783
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 SetOKResult(result, "");
1784 5 }
1785
1786 /*
1787 *
1788 */
1789 10325 void PlayerResource::GetE131BytesReceived(Json::Value& result) {
1790
1/2
✓ Branch 0 taken 10325 times.
✗ Branch 1 not taken.
10325 result = GetE131UniverseBytesReceived(); // e131bridge.cpp
1791
1792
1/2
✓ Branch 0 taken 10325 times.
✗ Branch 1 not taken.
10325 if (result.isMember("universes"))
1793
2/4
✓ Branch 0 taken 10325 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 10325 times.
✗ Branch 3 not taken.
20650 SetOKResult(result, "");
1794 else
1795 ✗ SetErrorResult(result, 400, "GetE131UniverseBytesReceived() did not return any universes.");
1796 10325 }
1797
1798 /*
1799 *
1800 */
1801 16307 void PlayerResource::GetMultiSyncSystems(Json::Value& result, bool localOnly) {
1802
1/2
✓ Branch 0 taken 16307 times.
✗ Branch 1 not taken.
16307 result = multiSync->GetSystems(localOnly);
1803
1804
1/2
✓ Branch 0 taken 16307 times.
✗ Branch 1 not taken.
16307 if (result.isMember("systems"))
1805
2/4
✓ Branch 0 taken 16307 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16307 times.
✗ Branch 3 not taken.
32614 SetOKResult(result, "");
1806 else
1807 ✗ SetErrorResult(result, 400, "MultiSync did not return any systems.");
1808 16307 }
1809
1810 89 void PlayerResource::GetMultiSyncStats(Json::Value& result, bool reset) {
1811
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 87 times.
89 if (reset)
1812 2 multiSync->ResetSyncStats();
1813
1814
1/2
✓ Branch 0 taken 89 times.
✗ Branch 1 not taken.
89 result = multiSync->GetSyncStats();
1815
1816
1/2
✓ Branch 0 taken 89 times.
✗ Branch 1 not taken.
89 if (result.isMember("systems"))
1817
2/4
✓ Branch 0 taken 89 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 89 times.
✗ Branch 3 not taken.
178 SetOKResult(result, "");
1818 else
1819 ✗ SetErrorResult(result, 400, "MultiSync did not return any systems.");
1820 89 }
1821
1822 /*
1823 *
1824 */
1825 5 void PlayerResource::GetPlaylistFileTime(Json::Value& result) {
1826
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 if (Player::INSTANCE.IsPlaying()) {
1827 5 uint64_t fileTime = Player::INSTANCE.GetFileTime();
1828
1829
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 result["fileTime"] = (Json::UInt64)fileTime;
1830 } else {
1831 ✗ result["fileTime"] = 0;
1832 }
1833
1834
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 SetOKResult(result, "");
1835 5 }
1836
1837 /*
1838 *
1839 */
1840 6 void PlayerResource::GetPlaylistConfig(Json::Value& result) {
1841
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 if (Player::INSTANCE.IsPlaying())
1842
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 result = Player::INSTANCE.GetConfig();
1843
1844
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
6 SetOKResult(result, "");
1845 6 }
1846
1847 /**
1848 * Report the configured Recurring Tasks merged with last-run status, for the
1849 * Recurring Tasks admin page (www/recurringtasks.php).
1850 *
1851 * @route GET /api/fppd/recurringtasks
1852 * @response 200 {"tasks": [...]}
1853 */
1854 55 void PlayerResource::GetRecurringTasks(Json::Value& result) {
1855
2/4
✓ Branch 0 taken 55 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 55 times.
✗ Branch 3 not taken.
55 result["tasks"] = Json::Value(Json::arrayValue);
1856 55 RecurringTasks::INSTANCE.reportStatus(result["tasks"]);
1857
2/4
✓ Branch 0 taken 55 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 55 times.
✗ Branch 3 not taken.
55 SetOKResult(result, "");
1858 55 }
1859
1860 /**
1861 * Preview a single If/Conditional Check leaf - the same lookup evaluate()
1862 * uses internally (Variable/Expression/Time/GPIO Pin/Sun), before any
1863 * comparator/Value is applied. Backs the If condition editor's "Show Current
1864 * Value" button, so picking the right Value to compare against isn't
1865 * guesswork.
1866 *
1867 * When a "comparator" param is also supplied (the consolidated eye-preview
1868 * modal's full-leaf mode), also evaluates Value the same way Value is
1869 * unconditionally evaluated at runtime and applies the comparator, returning
1870 * the RHS value and the boolean result too - reuses
1871 * ConditionNode::PreviewLeafResult(), the exact same evaluation path a real
1872 * saved leaf's evaluate() takes, so this can never drift from runtime
1873 * behavior.
1874 *
1875 * @route GET /api/fppd/condition/preview
1876 * @param string source One of the If Check "Source" dropdown values (e.g. "Variable", "GPIO Pin").
1877 * @param string name The Name/Expression field for that source.
1878 * @param string comparator Optional - one of the Check "Comparator" values. Enables full-leaf mode.
1879 * @param string value Optional - the Value field, only used when "comparator" is also given.
1880 * @param string not Optional - "true" to negate the result, only used when "comparator" is also given.
1881 * @response 200 {"found": true, "value": "..."} (source/name-only mode) or
1882 * {"found": true, "value": "...", "rhsValue": "...", "result": true} (full-leaf mode, LHS resolved) or
1883 * {"found": false, "rhsValue": "...", "result": true} (full-leaf mode, LHS not found - rhsValue/result
1884 * are still evaluated, since Value always evaluates and a missing LHS just yields `negate`) or
1885 * {"found": false} if the source/name doesn't currently resolve (source/name-only mode).
1886 */
1887 16 void PlayerResource::GetConditionPreview(const HttpRequestPtr& req, Json::Value& result) {
1888
2/4
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✗ Branch 3 not taken.
32 std::string source = getRequestArg(req, "source");
1889
2/4
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✗ Branch 3 not taken.
16 std::string name = getRequestArg(req, "name");
1890
4/6
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 12 times.
✓ Branch 5 taken 4 times.
48 if (req->getParameter("comparator").empty()) {
1891
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 LogDebug(VB_HTTP, "API - Getting condition preview for source \"%s\" name \"%s\"\n", source.c_str(), name.c_str());
1892 12 bool found = false;
1893
1/2
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
12 std::string value = ConditionNode::PreviewSourceValue(source, name, found);
1894
2/4
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
12 result["found"] = found;
1895
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 7 times.
12 if (found) {
1896
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 result["value"] = value;
1897 }
1898
2/4
✓ Branch 0 taken 12 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 12 times.
✗ Branch 3 not taken.
12 SetOKResult(result, "");
1899 12 return;
1900 12 }
1901
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
8 std::string comparator = getRequestArg(req, "comparator");
1902
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
8 std::string value = getRequestArg(req, "value");
1903
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.
4 bool negate = getRequestArg(req, "not") == "true";
1904
3/4
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 LogDebug(VB_HTTP, "API - Getting full condition preview for source \"%s\" name \"%s\" %s%s \"%s\"\n",
1905 source.c_str(), name.c_str(), negate ? "NOT " : "", comparator.c_str(), value.c_str());
1906 4 bool lhsFound = false;
1907 4 std::string lhsValue, rhsValue;
1908 4 bool leafResult = false;
1909
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 ConditionNode::PreviewLeafResult(source, name, comparator, value, negate, lhsFound, lhsValue, rhsValue, leafResult);
1910
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 result["found"] = lhsFound;
1911
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1 times.
4 if (lhsFound) {
1912
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 result["value"] = lhsValue;
1913 }
1914 // rhsValue/result are always resolvable (Value is unconditionally
1915 // evaluated, and a not-found LHS still yields a definite result of
1916 // `negate`), so return them regardless of lhsFound - lets the eye-preview
1917 // modal show what Value evaluates to instead of stopping cold when only
1918 // the LHS side is missing.
1919
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 result["rhsValue"] = rhsValue;
1920
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 result["result"] = leafResult;
1921
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 SetOKResult(result, "");
1922
4/4
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 12 times.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 12 times.
28 }
1923
1924 /*
1925 *
1926 */
1927 187 void PlayerResource::PostEffects(const std::string& effectName, const Json::Value& data, Json::Value& result) {
1928
4/4
✓ Branch 0 taken 119 times.
✓ Branch 1 taken 68 times.
✓ Branch 2 taken 85 times.
✓ Branch 3 taken 34 times.
187 if (!data.isMember("command")) {
1929
2/4
✓ Branch 0 taken 85 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 85 times.
✗ Branch 3 not taken.
85 SetErrorResult(result, 400, "'command' field not specified");
1930 85 return;
1931 }
1932
1933
3/4
✓ Branch 0 taken 34 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✓ Branch 3 taken 17 times.
34 std::string command = data["command"].asString();
1934
1935
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 LogDebug(VB_HTTP, "API - PostEffect(%s) cmd: %s\n", effectName.c_str(), command.c_str());
1936
1937
2/4
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
17 SetOKResult(result, "PostEffect result would go here");
1938 17 }
1939
1940 /*
1941 *
1942 */
1943 ✗ void PlayerResource::PostFalconHardware(Json::Value& result) {
1944 ✗ LogDebug(VB_HTTP, "API - Reloading hardware config\n");
1945
1946 ✗ SetOKResult(result, "PostFalconHardware result would go here");
1947 ✗ }
1948
1949 /*
1950 *
1951 */
1952 11 void PlayerResource::PostGPIOExt(const Json::Value& data, Json::Value& result) {
1953
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
11 if (!data.isMember("config")) {
1954
2/4
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
7 SetErrorResult(result, 400, "'config' field not specified");
1955 7 return;
1956 }
1957
1958 ✗ LogDebug(VB_HTTP, "API - Doing something with gpio\n");
1959 }
1960
1961 /*
1962 *
1963 */
1964 14 void PlayerResource::PostOutputs(const Json::Value& data, Json::Value& result) {
1965
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 5 times.
14 if (!data.isMember("command")) {
1966
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 SetErrorResult(result, 400, "'command' field not specified");
1967 5 return;
1968 }
1969
1970
6/8
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✓ Branch 7 taken 3 times.
5 if (data["command"].asString() == "enable") {
1971 1 EnableChannelOutput();
1972
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 SetOKResult(result, "channel output enabled");
1973
5/8
✓ 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.
✓ Branch 6 taken 2 times.
✓ Branch 7 taken 1 times.
3 } else if (data["command"].asString() == "disable") {
1974 2 DisableChannelOutput();
1975
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
4 SetOKResult(result, "channel output disabled");
1976 }
1977 }
1978
1979 /*
1980 *
1981 */
1982 11 void PlayerResource::PostOutputsRemap(const Json::Value& data, Json::Value& result) {
1983
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 2 times.
11 if (!data.isMember("command")) {
1984
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 SetErrorResult(result, 400, "'command' field not specified");
1985 5 return;
1986 }
1987
1988
5/8
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 1 times.
2 if (data["command"].asString() == "reload") {
1989 // FIXME, need to fix this function to lock the remap array
1990 // LoadChannelRemapData();
1991 ✗ SetOKResult(result, "channel remaps reloaded");
1992 }
1993 }
1994
1995 /**
1996 * Re-read config/recurringtasks.json and re-schedule all Recurring Task
1997 * timers to match, so a save on www/recurringtasks.php (which writes that
1998 * file via the generic api/configfile endpoint) takes effect without an
1999 * fppd restart. Also handles an immediate "Test Run" of one task, run
2000 * synchronously against the posted task definition (not a saved name), so
2001 * the admin page can preview unsaved edits.
2002 *
2003 * @route POST /api/fppd/recurringtasks
2004 * @body {"command": "reload"}
2005 * @body {"command": "test", "task": {"name": "...", "type": "command", "command": "URL", "args": [...], "filterType": "json", "filterJsonPath": "data.temp"}}
2006 * @response 200 Recurring tasks reloaded, or {"ok":bool,"raw":string,"filtered":string,"error":string} for "test".
2007 * @response 400 'command' field not specified.
2008 */
2009 32 void PlayerResource::PostRecurringTasks(const Json::Value& data, Json::Value& result) {
2010
4/4
✓ Branch 0 taken 28 times.
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 23 times.
32 if (!data.isMember("command")) {
2011
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 SetErrorResult(result, 400, "'command' field not specified");
2012 5 return;
2013 }
2014
2015
3/4
✓ Branch 0 taken 23 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 22 times.
✓ Branch 3 taken 1 times.
23 std::string command = data["command"].asString();
2016
3/4
✓ Branch 0 taken 22 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 17 times.
22 if (command == "reload") {
2017
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 RecurringTasks::INSTANCE.Reload();
2018
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
10 SetOKResult(result, "Recurring tasks reloaded");
2019
3/4
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✓ Branch 3 taken 1 times.
17 } else if (command == "test") {
2020
3/4
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✓ Branch 3 taken 2 times.
16 Json::Value testResult = RecurringTasks::INSTANCE.TestRunTask(data["task"]);
2021
3/4
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 57 times.
✓ Branch 3 taken 14 times.
71 for (auto const& key : testResult.getMemberNames()) {
2022
3/6
✓ Branch 0 taken 57 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 57 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 57 times.
✗ Branch 5 not taken.
57 result[key] = testResult[key];
2023 14 }
2024
2/4
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✗ Branch 3 not taken.
14 SetOKResult(result, "");
2025 14 }
2026 22 }
2027
2028 /*
2029 *
2030 */
2031 257 void PlayerResource::PostSchedule(const Json::Value data, Json::Value& result) {
2032
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 248 times.
257 if (!data.isMember("command")) {
2033
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 SetErrorResult(result, 400, "'command' field not specified");
2034 5 return;
2035 }
2036
2037
6/8
✓ Branch 0 taken 248 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 247 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 247 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 246 times.
✓ Branch 7 taken 1 times.
248 if (data["command"].asString() == "reload") {
2038 246 scheduler->ReloadScheduleFile();
2039
2040
2/4
✓ Branch 0 taken 246 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 246 times.
✗ Branch 3 not taken.
492 SetOKResult(result, "Schedule reload triggered");
2041 }
2042 }
2043
2044 25736 void PlayerResource::periodicWork() {
2045
1/2
✓ Branch 0 taken 3832 times.
✗ Branch 1 not taken.
25736 if (piPowerFile > 0) {
2046 #ifdef PLATFORM_PI
2047 3832 int i = 0;
2048 int32_t p[(MAX_STRING >> 2) + 7];
2049 3832 p[i++] = 0; // size
2050 3832 p[i++] = 0x00000000; // process request
2051
2052 3832 p[i++] = GET_GENCMD_RESULT; // (the tag id)
2053 3832 p[i++] = MAX_STRING; // buffer_len
2054 3832 p[i++] = 0; // request_len (set to response length)
2055 3832 p[i++] = 0; // error repsonse
2056
2057 3832 memcpy(p + i, "get_throttled", strlen("get_throttled") + 1);
2058 3832 i += MAX_STRING >> 2;
2059 3832 p[i++] = 0x00000000; // end tag
2060 3832 p[0] = i * sizeof *p; // actual size
2061
2062 3832 int ret_val = ioctl(piPowerFile, IOCTL_MBOX_PROPERTY, p);
2063
1/2
✓ Branch 0 taken 3832 times.
✗ Branch 1 not taken.
3832 std::string s = (char*)&p[6];
2064
1/2
✓ Branch 0 taken 3832 times.
✗ Branch 1 not taken.
3832 if (s.starts_with("throttled=0x")) {
2065
1/2
✓ Branch 0 taken 3832 times.
✗ Branch 1 not taken.
3832 s = s.substr(12);
2066
1/2
✓ Branch 0 taken 3832 times.
✗ Branch 1 not taken.
3832 uint32_t res = std::stol(s, nullptr, 16);
2067 3832 bool bad = res & 0x1;
2068 3832 piPowerBad = bad;
2069 // Shared by the add and the remove: RemoveWarning() matches on the
2070 // exact text.
2071 3832 constexpr int PI_POWER_WARNING_ID = 15;
2072 3832 constexpr const char* PI_POWER_WARNING_MSG = "Raspberry Pi Voltage Too Low";
2073 // The banner goes up after more than 5 bad readings in a row and
2074 // comes down after more than 5 good ones, so a supply hovering at
2075 // the threshold doesn't flap it. It used to stay up until fppd
2076 // restarted, long after the supply had been fixed.
2077 static int goodCount = 0;
2078
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3832 times.
3832 if (bad) {
2079 ✗ goodCount = 0;
2080 ✗ piPowerWarningCount++;
2081 ✗ if (piPowerWarningCount > 5 && !piPowerWarningAdded) {
2082 ✗ WarningHolder::AddWarning(PI_POWER_WARNING_ID, PI_POWER_WARNING_MSG);
2083 ✗ piPowerWarningAdded = true;
2084 }
2085 } else {
2086 3832 piPowerWarningCount = 0;
2087
2/6
✗ Branch 0 not taken.
✓ Branch 1 taken 3832 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 3832 times.
3832 if (piPowerWarningAdded && ++goodCount > 5) {
2088 ✗ WarningHolder::RemoveWarning(PI_POWER_WARNING_ID, PI_POWER_WARNING_MSG);
2089 ✗ piPowerWarningAdded = false;
2090 ✗ goodCount = 0;
2091 }
2092 }
2093 }
2094 #endif
2095 3832 }
2096 25736 }
2097
2098 25736 void APIServer::periodicWork() {
2099
1/2
✓ Branch 0 taken 25736 times.
✗ Branch 1 not taken.
25736 if (m_pr) {
2100 25736 m_pr->periodicWork();
2101 }
2102 25736 }
2103