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