GCC Code Coverage Report


Directory: ./
File: Variables.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 345 408 84.6%
Functions: 33 36 91.7%
Branches: 478 981 48.7%

Line Branch Exec Source
1 /*
2 * This file is part of the Falcon Player (FPP) and is Copyright (C)
3 * 2013-2026 by the Falcon Player Developers.
4 *
5 * The Falcon Player (FPP) is free software, and is covered under
6 * multiple Open Source licenses. Please see the included 'LICENSES'
7 * file for descriptions of what files are covered by each license.
8 *
9 * This source file is covered under the LGPL v2.1 as described in the
10 * included LICENSE.LGPL file.
11 */
12
13 #include "fpp-pch.h"
14
15 #include "fpp-json.h"
16 #include "fpphttp.h"
17 #include "fpphttp_clientip.h" // getEffectiveClientIP() - FPP-internal, not in the plugin API
18
19 #include "MultiSync.h"
20 #include "Player.h"
21 #include "Scheduler.h"
22 #include "Variables.h"
23 #include "Warnings.h"
24 #include "commands/Commands.h"
25 #include "commands/Condition.h"
26 #include "common.h"
27 #include "log.h"
28 #include "mqtt.h"
29 #include "settings.h"
30 #include "mediaoutput/mediaoutput.h"
31 #include "util/ExpressionProcessor.h"
32
33 #include <condition_variable>
34 #include <ctime>
35 #include <map>
36 #include <utility>
37
38 // std::to_string(double) always pads to 6 decimals ("45.000000"); use a
39 // format that drops trailing zeros so integer-valued results stay readable.
40 96 static std::string DoubleToCleanString(double v) {
41 char buf[64];
42 96 snprintf(buf, sizeof(buf), "%g", v);
43
1/2
✓ Branch 0 taken 96 times.
✗ Branch 1 not taken.
192 return std::string(buf);
44 }
45
46 // Same "static initialized at process start" convention as httpAPI.cpp's own
47 // (private) startupTime - duplicated rather than shared across translation
48 // units for one time_t.
49 // On the monotonic clock, as httpAPI.cpp's: a wall-clock step made the
50 // uptime negative or hours off.
51 static long long FPP_STARTUP_MS = GetMonotonicTimeMS();
52
53 // FPP_STATUS_* -> name, mirroring GetCurrentFPPDStatus()'s switch
54 // (httpAPI.cpp) - not shared code since it's a small, stable 6-case mapping
55 // and pulling in httpAPI.cpp's own dependencies here isn't worth it.
56 119915 static std::string PlaylistStatusName(int status) {
57
5/7
✓ Branch 0 taken 6244 times.
✓ Branch 1 taken 104571 times.
✓ Branch 2 taken 28 times.
✓ Branch 3 taken 56 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 9016 times.
✗ Branch 6 not taken.
119915 switch (status) {
58 6244 case 0:
59
1/2
✓ Branch 0 taken 6244 times.
✗ Branch 1 not taken.
12488 return "idle";
60 104571 case 1:
61
1/2
✓ Branch 0 taken 104571 times.
✗ Branch 1 not taken.
209142 return "playing";
62 28 case 2:
63
1/2
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
56 return "stopping gracefully";
64 56 case 3:
65
1/2
✓ Branch 0 taken 56 times.
✗ Branch 1 not taken.
112 return "stopping gracefully after loop";
66 ✗ case 4:
67 ✗ return "stopping now";
68 9016 case 5:
69
1/2
✓ Branch 0 taken 9016 times.
✗ Branch 1 not taken.
18032 return "paused";
70 ✗ default:
71 ✗ return "unknown";
72 }
73 }
74
75 // Playlist::GetCurrentStatus() sets some fields (e.g. "repeat_mode",
76 // "random") as a JSON int in its active-playlist branch but as a JSON
77 // string in its idle-state default branch - Json::Value::asString() throws
78 // if the value isn't actually a string, so a plain .asString() here would
79 // throw depending on player state. Coerce defensively regardless of type.
80 1319065 static std::string JsonToStr(const Json::Value& v) {
81
2/2
✓ Branch 0 taken 1085479 times.
✓ Branch 1 taken 233586 times.
1319065 if (v.isString()) {
82 1085479 return v.asString();
83 }
84
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 233586 times.
233586 if (v.isBool()) {
85 ✗ return v.asBool() ? "1" : "0";
86 }
87
1/2
✓ Branch 0 taken 233586 times.
✗ Branch 1 not taken.
233586 if (v.isIntegral()) {
88 233586 return std::to_string(v.asInt64());
89 }
90 ✗ if (v.isDouble()) {
91 ✗ return DoubleToCleanString(v.asDouble());
92 }
93 ✗ return "";
94 }
95
96 // Computes the current value of every "fpp_" read-only status variable.
97 // Cheap (a handful of in-memory reads, no I/O) so recomputed fresh on every
98 // call rather than cached - avoids any staleness question.
99 //
100 // Underscore, not the old hyphen ("fpp_") this family used previously: a
101 // bare "fpp_next_playlist_start" is directly usable in pure-math mode
102 // (lowercase-led, a-z0-9_ only - see ExpressionProcessor's
103 // isValidExprIdentifier()) with no aliasing needed, unlike the old
104 // hyphenated form. Renamed outright, not kept as a dual-registered alias -
105 // per explicit direction, no old "fpp-*" references remain anywhere.
106 119915 static std::map<std::string, std::string> ComputeFppStatusVariables() {
107 119915 std::map<std::string, std::string> vars;
108
109
1/2
✓ Branch 0 taken 119915 times.
✗ Branch 1 not taken.
119915 int status = (int)Player::INSTANCE.GetStatus();
110
2/4
✓ Branch 0 taken 119915 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 119915 times.
✗ Branch 3 not taken.
359745 vars["fpp_status"] = std::to_string(status);
111
3/6
✓ Branch 0 taken 119915 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 119915 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 119915 times.
✗ Branch 5 not taken.
359745 vars["fpp_status_name"] = PlaylistStatusName(status);
112
5/10
✓ Branch 0 taken 119915 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 119915 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 119915 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 119915 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 119915 times.
✗ Branch 9 not taken.
359745 vars["fpp_mode_name"] = toStdStringAndFree(modeToString(getFPPmode()));
113
3/6
✓ Branch 0 taken 119915 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 119915 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 119915 times.
✗ Branch 5 not taken.
359745 vars["fpp_volume"] = std::to_string(getVolume());
114
5/10
✓ Branch 0 taken 119915 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 119915 times.
✓ Branch 4 taken 119915 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 119915 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 119915 times.
✗ Branch 9 not taken.
239830 vars["fpp_multisync"] = (multiSync && multiSync->isMultiSyncEnabled()) ? "1" : "0";
115
4/8
✓ Branch 0 taken 119915 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 119915 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 119915 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 119915 times.
✗ Branch 7 not taken.
359745 vars["fpp_uptime_seconds"] = std::to_string((GetMonotonicTimeMS() - FPP_STARTUP_MS) / 1000);
116
6/10
✓ Branch 0 taken 119915 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 113671 times.
✓ Branch 3 taken 6244 times.
✓ Branch 4 taken 119915 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 119915 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 119915 times.
✗ Branch 9 not taken.
239830 vars["fpp_is_playing"] = Player::INSTANCE.IsPlaying() ? "1" : "0";
117
6/10
✓ Branch 0 taken 119915 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 112 times.
✓ Branch 3 taken 119803 times.
✓ Branch 4 taken 119915 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 119915 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 119915 times.
✗ Branch 9 not taken.
239830 vars["fpp_was_scheduled"] = Player::INSTANCE.WasScheduled() ? "1" : "0";
118
5/10
✓ Branch 0 taken 119915 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 119915 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 119915 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 119915 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 119915 times.
✗ Branch 9 not taken.
239830 vars["fpp_scheduler_enabled"] = (scheduler && scheduler->IsEnabled()) ? "1" : "0";
119 {
120 // Same "HH:MM" convention as the "Time" Condition source
121 // (Condition.cpp's CurrentTimeHHMM()) so a plain fpp_current_time
122 // comparison lines up with what that source's Value examples show.
123 // Date is ISO 8601 (YYYY-MM-DD) - unambiguous and sorts/compares
124 // correctly as a plain string. Day name matches strftime's locale-
125 // independent full weekday name (English), same as the rest of this
126 // list rather than the system locale.
127 static const char* kDayNames[7] = { "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" };
128 static const char* kMonthNames[12] = { "January", "February", "March", "April", "May", "June",
129 "July", "August", "September", "October", "November", "December" };
130 119915 std::time_t now = std::time(nullptr);
131 struct tm local;
132 119915 localtime_r(&now, &local);
133 char buf[16];
134 119915 snprintf(buf, sizeof(buf), "%02d:%02d", local.tm_hour, local.tm_min);
135
3/6
✓ Branch 0 taken 119915 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 119915 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 119915 times.
✗ Branch 5 not taken.
119915 vars["fpp_current_time"] = buf;
136 119915 snprintf(buf, sizeof(buf), "%04d-%02d-%02d", local.tm_year + 1900, local.tm_mon + 1, local.tm_mday);
137
3/6
✓ Branch 0 taken 119915 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 119915 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 119915 times.
✗ Branch 5 not taken.
119915 vars["fpp_current_date"] = buf;
138
3/6
✓ Branch 0 taken 119915 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 119915 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 119915 times.
✗ Branch 5 not taken.
239830 vars["fpp_day_of_week"] = kDayNames[local.tm_wday];
139
3/6
✓ Branch 0 taken 119915 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 119915 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 119915 times.
✗ Branch 5 not taken.
239830 vars["fpp_current_month"] = kMonthNames[local.tm_mon];
140 }
141
4/8
✓ Branch 0 taken 119915 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 119915 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 119915 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 119915 times.
✗ Branch 7 not taken.
359745 vars["fpp_warning_count"] = std::to_string(WarningHolder::GetWarnings().size());
142
1/2
✓ Branch 0 taken 119915 times.
✗ Branch 1 not taken.
119915 if (scheduler) {
143
3/6
✓ Branch 0 taken 119915 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 119915 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 119915 times.
✗ Branch 5 not taken.
359745 vars["fpp_next_playlist"] = scheduler->GetNextPlaylistName();
144
3/6
✓ Branch 0 taken 119915 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 119915 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 119915 times.
✗ Branch 5 not taken.
359745 vars["fpp_next_playlist_start"] = scheduler->GetNextPlaylistStartStr();
145 } else {
146 ✗ vars["fpp_next_playlist"] = "";
147 ✗ vars["fpp_next_playlist_start"] = "";
148 }
149
150
1/2
✓ Branch 0 taken 119915 times.
✗ Branch 1 not taken.
119915 Json::Value pl;
151
1/2
✓ Branch 0 taken 119915 times.
✗ Branch 1 not taken.
119915 Player::INSTANCE.GetCurrentStatus(pl);
152
5/10
✓ Branch 0 taken 119915 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 119915 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 119915 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 119915 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 119915 times.
✗ Branch 9 not taken.
359745 vars["fpp_current_playlist"] = JsonToStr(pl["current_playlist"]["playlist"]);
153
5/10
✓ Branch 0 taken 119915 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 119915 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 119915 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 119915 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 119915 times.
✗ Branch 9 not taken.
359745 vars["fpp_current_playlist_count"] = JsonToStr(pl["current_playlist"]["count"]);
154
5/10
✓ Branch 0 taken 119915 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 119915 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 119915 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 119915 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 119915 times.
✗ Branch 9 not taken.
359745 vars["fpp_current_playlist_index"] = JsonToStr(pl["current_playlist"]["index"]);
155
4/8
✓ Branch 0 taken 119915 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 119915 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 119915 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 119915 times.
✗ Branch 7 not taken.
359745 vars["fpp_current_sequence"] = JsonToStr(pl["current_sequence"]);
156
4/8
✓ Branch 0 taken 119915 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 119915 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 119915 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 119915 times.
✗ Branch 7 not taken.
359745 vars["fpp_current_song"] = JsonToStr(pl["current_song"]);
157
4/8
✓ Branch 0 taken 119915 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 119915 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 119915 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 119915 times.
✗ Branch 7 not taken.
359745 vars["fpp_seconds_played"] = JsonToStr(pl["seconds_played"]);
158
4/8
✓ Branch 0 taken 119915 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 119915 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 119915 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 119915 times.
✗ Branch 7 not taken.
359745 vars["fpp_seconds_remaining"] = JsonToStr(pl["seconds_remaining"]);
159
4/8
✓ Branch 0 taken 119915 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 119915 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 119915 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 119915 times.
✗ Branch 7 not taken.
359745 vars["fpp_time_elapsed"] = JsonToStr(pl["time_elapsed"]);
160
4/8
✓ Branch 0 taken 119915 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 119915 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 119915 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 119915 times.
✗ Branch 7 not taken.
359745 vars["fpp_time_remaining"] = JsonToStr(pl["time_remaining"]);
161
4/8
✓ Branch 0 taken 119915 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 119915 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 119915 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 119915 times.
✗ Branch 7 not taken.
359745 vars["fpp_repeat_mode"] = JsonToStr(pl["repeat_mode"]);
162
4/8
✓ Branch 0 taken 119915 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 119915 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 119915 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 119915 times.
✗ Branch 7 not taken.
359745 vars["fpp_random"] = JsonToStr(pl["random"]);
163
164 119915 return vars;
165 119915 }
166
167 Variables Variables::INSTANCE;
168
169 2346 Variables::Variables() {
170 2346 }
171 458 Variables::~Variables() {
172 458 }
173
174 1885 void Variables::Init() {
175 1885 load();
176 1885 }
177
178 1867 void Variables::Close() {
179 1867 save();
180 1867 }
181
182 1885 void Variables::load() {
183
3/6
✓ 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.
1885 std::string file = FPP_DIR_CONFIG("/variables.json");
184
3/4
✓ Branch 0 taken 1885 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1865 times.
✓ Branch 3 taken 20 times.
1885 if (!FileExists(file)) {
185 1865 return;
186 }
187
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 Json::Value root = LoadJsonFromFile(file, JsonRoot::Object);
188
1/2
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
20 std::unique_lock<std::mutex> lock(m_lock);
189
3/4
✓ Branch 0 taken 20 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 20 times.
22 for (auto const& name : root.getMemberNames()) {
190 // A hand-edited variable of the wrong JSON type threw at startup, to
191 // std::terminate; skip that one variable instead.
192 try {
193 2 VariableEntry e;
194
4/6
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 1 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 1 times.
2 e.value = root[name]["value"].asString();
195 ✗ e.persist = root[name]["persist"].asBool();
196 ✗ e.lastUpdated = (time_t)root[name]["lastUpdated"].asInt64();
197 ✗ m_variables[name] = e;
198
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
4 } catch (const std::exception& ex) {
199
4/8
✓ 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.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
6 WarningHolder::InvalidConfigEntry("variables.json", name + ": " + ex.what());
200 2 }
201 20 }
202
2/2
✓ Branch 0 taken 20 times.
✓ Branch 1 taken 1865 times.
1885 }
203
204 // Held across a save's snapshot *and* its write. With only m_lock around
205 // the snapshot, two threads saving at once could write in the opposite
206 // order to their snapshots, leaving the older values on disk. Always taken
207 // before m_lock, never inside it.
208 static std::mutex s_saveLock;
209
210 2171 void Variables::save() {
211
1/2
✓ Branch 0 taken 2171 times.
✗ Branch 1 not taken.
2171 std::unique_lock<std::mutex> saveLock(s_saveLock);
212
1/2
✓ Branch 0 taken 2171 times.
✗ Branch 1 not taken.
2171 Json::Value root;
213
1/2
✓ Branch 0 taken 2171 times.
✗ Branch 1 not taken.
2171 std::unique_lock<std::mutex> lock(m_lock);
214
2/2
✓ Branch 0 taken 722 times.
✓ Branch 1 taken 2171 times.
2893 for (auto const& kv : m_variables) {
215
2/2
✓ Branch 0 taken 307 times.
✓ Branch 1 taken 415 times.
722 if (kv.second.persist) {
216
3/6
✓ Branch 0 taken 307 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 307 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 307 times.
✗ Branch 5 not taken.
307 root[kv.first]["value"] = kv.second.value;
217
3/6
✓ Branch 0 taken 307 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 307 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 307 times.
✗ Branch 5 not taken.
307 root[kv.first]["persist"] = true;
218
3/6
✓ Branch 0 taken 307 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 307 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 307 times.
✗ Branch 5 not taken.
307 root[kv.first]["lastUpdated"] = (Json::Int64)kv.second.lastUpdated;
219 }
220 }
221
1/2
✓ Branch 0 taken 2171 times.
✗ Branch 1 not taken.
2171 lock.unlock();
222
5/10
✓ Branch 0 taken 2171 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2171 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2171 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2171 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2171 times.
✗ Branch 9 not taken.
6513 SaveJsonToFile(root, FPP_DIR_CONFIG("/variables.json"), "\t");
223 2171 }
224
225 99192 bool Variables::IsFppStatusVariableName(const std::string& name) {
226 // "fpp_" (underscore) - a bare "fpp_next_playlist_start" is directly
227 // usable in pure-math mode with no aliasing needed (lowercase-led,
228 // a-z0-9_ only - see ExpressionProcessor's isValidExprIdentifier()),
229 // unlike the old hyphenated "fpp-" form this replaced.
230 99192 return name.rfind("fpp_", 0) == 0;
231 }
232
233 // "mqtt-<topic>" is a live, read-only variable backed directly by
234 // MosquittoClient's own last-message-per-topic cache (mqtt.h's
235 // messageCache) rather than the persisted m_variables map - same shape as
236 // the "fpp_" branch above. <topic> is the raw MQTT topic string (may
237 // contain '/'), so it's only usable where the value is looked up by name
238 // (variable condition leaf, %VAR:name% substitution) - it won't parse as
239 // an ExpressionProcessor/tinyexpr identifier, which requires alnum/underscore.
240 11529 bool Variables::IsMqttVariableName(const std::string& name) {
241 11529 return name.rfind("mqtt-", 0) == 0;
242 }
243
244 93810 std::string Variables::getVariable(const std::string& name, const std::string& def) {
245
3/4
✓ Branch 0 taken 93810 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 87408 times.
✓ Branch 3 taken 6402 times.
93810 if (IsFppStatusVariableName(name)) {
246
1/2
✓ Branch 0 taken 87408 times.
✗ Branch 1 not taken.
87408 auto vars = ComputeFppStatusVariables();
247
1/2
✓ Branch 0 taken 87408 times.
✗ Branch 1 not taken.
87408 auto it = vars.find(name);
248
2/4
✗ Branch 0 not taken.
✓ Branch 1 taken 87408 times.
✓ Branch 2 taken 87408 times.
✗ Branch 3 not taken.
87408 return it == vars.end() ? def : it->second;
249 87408 }
250
3/4
✓ Branch 0 taken 6402 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 477 times.
✓ Branch 3 taken 5925 times.
6402 if (IsMqttVariableName(name)) {
251
1/2
✓ Branch 0 taken 477 times.
✗ Branch 1 not taken.
477 std::string topic = name.substr(5);
252 477 std::string value;
253
4/8
✓ Branch 0 taken 477 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 477 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 477 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 477 times.
✗ Branch 7 not taken.
477 if (mqtt && mqtt->GetCachedMessage(topic, value)) {
254 477 return value;
255 }
256 ✗ return def;
257 477 }
258
1/2
✓ Branch 0 taken 5925 times.
✗ Branch 1 not taken.
5925 std::unique_lock<std::mutex> lock(m_lock);
259
1/2
✓ Branch 0 taken 5925 times.
✗ Branch 1 not taken.
5925 auto it = m_variables.find(name);
260
2/2
✓ Branch 0 taken 137 times.
✓ Branch 1 taken 5788 times.
5925 if (it == m_variables.end()) {
261
1/2
✓ Branch 0 taken 137 times.
✗ Branch 1 not taken.
137 return def;
262 }
263
1/2
✓ Branch 0 taken 5788 times.
✗ Branch 1 not taken.
5788 return it->second.value;
264 5925 }
265 ✗ int Variables::getVariableInt(const std::string& name, int def) {
266 ✗ std::string v = getVariable(name, "");
267 ✗ if (v.empty()) {
268 ✗ return def;
269 }
270 try {
271 ✗ return std::stoi(v);
272 ✗ } catch (...) {
273 ✗ return def;
274 ✗ }
275 ✗ }
276 ✗ double Variables::getVariableDouble(const std::string& name, double def) {
277 ✗ std::string v = getVariable(name, "");
278 ✗ if (v.empty()) {
279 ✗ return def;
280 }
281 try {
282 ✗ return std::stod(v);
283 ✗ } catch (...) {
284 ✗ return def;
285 ✗ }
286 ✗ }
287 ✗ bool Variables::getVariableBool(const std::string& name, bool def) {
288 ✗ std::string v = getVariable(name, "");
289 ✗ if (v.empty()) {
290 ✗ return def;
291 }
292 ✗ return v == "true" || v == "1";
293 ✗ }
294 7227 std::vector<std::string> Variables::getVariableNames() {
295 7227 std::vector<std::string> names;
296
1/2
✓ Branch 0 taken 7227 times.
✗ Branch 1 not taken.
7227 std::unique_lock<std::mutex> lock(m_lock);
297
1/2
✓ Branch 0 taken 7227 times.
✗ Branch 1 not taken.
7227 names.reserve(m_variables.size());
298
2/2
✓ Branch 0 taken 12070 times.
✓ Branch 1 taken 7227 times.
19297 for (auto const& kv : m_variables) {
299
1/2
✓ Branch 0 taken 12070 times.
✗ Branch 1 not taken.
12070 names.push_back(kv.first);
300 }
301 7227 return names;
302 7227 }
303
304 7227 std::vector<std::string> Variables::getAllVariableNames() {
305
1/2
✓ Branch 0 taken 7227 times.
✗ Branch 1 not taken.
7227 std::vector<std::string> names = getVariableNames();
306
307 // Names only: no need for the change tracking reportFppVariables() does.
308
3/4
✓ Branch 0 taken 7227 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 195129 times.
✓ Branch 3 taken 7227 times.
202356 for (auto const& kv : ComputeFppStatusVariables()) {
309
1/2
✓ Branch 0 taken 195129 times.
✗ Branch 1 not taken.
195129 names.push_back(kv.first);
310 7227 }
311
312
1/2
✓ Branch 0 taken 7227 times.
✗ Branch 1 not taken.
7227 Json::Value mqttVars;
313
1/2
✓ Branch 0 taken 7227 times.
✗ Branch 1 not taken.
7227 reportMqttVariables(mqttVars);
314
3/4
✓ Branch 0 taken 7227 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 477 times.
✓ Branch 3 taken 7227 times.
7704 for (auto const& name : mqttVars.getMemberNames()) {
315
1/2
✓ Branch 0 taken 477 times.
✗ Branch 1 not taken.
477 names.push_back(name);
316 7227 }
317
318 7227 return names;
319 7227 }
320
321 4872 void Variables::setVariable(const std::string& name, const std::string& value, bool persist) {
322
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4872 times.
4872 if (IsFppStatusVariableName(name)) {
323 ✗ LogWarn(VB_GENERAL, "Variables::setVariable(): \"%s\" starts with the reserved \"fpp_\" prefix "
324 "(read-only computed status) and cannot be set\n",
325 name.c_str());
326 ✗ return;
327 }
328
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4872 times.
4872 if (IsMqttVariableName(name)) {
329 ✗ LogWarn(VB_GENERAL, "Variables::setVariable(): \"%s\" starts with the reserved \"mqtt-\" prefix "
330 "(read-only, backed by the MQTT message cache) and cannot be set\n",
331 name.c_str());
332 ✗ return;
333 }
334 4872 bool wasPersisted = false;
335 {
336
1/2
✓ Branch 0 taken 4872 times.
✗ Branch 1 not taken.
4872 std::unique_lock<std::mutex> lock(m_lock);
337
1/2
✓ Branch 0 taken 4872 times.
✗ Branch 1 not taken.
4872 VariableEntry& e = m_variables[name];
338 4872 wasPersisted = e.persist;
339
1/2
✓ Branch 0 taken 4872 times.
✗ Branch 1 not taken.
4872 e.value = value;
340 4872 e.persist = persist;
341 4872 e.lastUpdated = time(nullptr);
342 4872 }
343 4872 fireListeners(name, value);
344 // Save whenever the variable is (or was) persisted, so demoting persist
345 // true->false actually removes the stale entry from variables.json
346 // instead of leaving it there until the next persisted write elsewhere.
347
3/4
✓ Branch 0 taken 4568 times.
✓ Branch 1 taken 304 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 4568 times.
4872 if (persist || wasPersisted) {
348 304 save();
349 }
350 }
351 5 void Variables::setVariable(const std::string& name, int value, bool persist) {
352
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 setVariable(name, std::to_string(value), persist);
353 5 }
354
355 255 bool Variables::deleteVariable(const std::string& name) {
356 255 bool existed = false;
357 255 bool wasPersisted = false;
358 {
359
1/2
✓ Branch 0 taken 255 times.
✗ Branch 1 not taken.
255 std::unique_lock<std::mutex> lock(m_lock);
360
1/2
✓ Branch 0 taken 255 times.
✗ Branch 1 not taken.
255 auto it = m_variables.find(name);
361
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 255 times.
255 if (it != m_variables.end()) {
362 ✗ existed = true;
363 ✗ wasPersisted = it->second.persist;
364 ✗ m_variables.erase(it);
365 }
366 255 }
367
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 255 times.
255 if (wasPersisted) {
368 ✗ save();
369 }
370 255 return existed;
371 }
372 48 double Variables::incrementVariable(const std::string& name, double delta, bool persist) {
373 48 double newValue = 0.0;
374 48 bool wasPersisted = false;
375 {
376
1/2
✓ Branch 0 taken 48 times.
✗ Branch 1 not taken.
48 std::unique_lock<std::mutex> lock(m_lock);
377
1/2
✓ Branch 0 taken 48 times.
✗ Branch 1 not taken.
48 VariableEntry& e = m_variables[name];
378 48 wasPersisted = e.persist;
379 48 double cur = 0.0;
380 try {
381
3/4
✓ Branch 0 taken 24 times.
✓ Branch 1 taken 24 times.
✓ Branch 2 taken 24 times.
✗ Branch 3 not taken.
48 cur = e.value.empty() ? 0.0 : std::stod(e.value);
382 ✗ } catch (...) {
383 ✗ cur = 0.0;
384 ✗ }
385 48 newValue = cur + delta;
386
1/2
✓ Branch 0 taken 48 times.
✗ Branch 1 not taken.
48 e.value = DoubleToCleanString(newValue);
387 48 e.persist = persist;
388 48 e.lastUpdated = time(nullptr);
389 48 }
390
2/4
✓ Branch 0 taken 48 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 48 times.
✗ Branch 3 not taken.
48 fireListeners(name, DoubleToCleanString(newValue));
391
2/4
✓ Branch 0 taken 48 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 48 times.
48 if (persist || wasPersisted) {
392 ✗ save();
393 }
394 48 return newValue;
395 }
396
397 // fireListeners() runs copies of the callbacks outside m_lock, so a listener
398 // could still be running after unregisterListener() returned, and its owner
399 // then freed what it uses. Count the calls in flight per (name, id), under
400 // m_lock, and have unregisterListener() wait for its listener's to finish.
401 static std::map<std::pair<std::string, std::string>, int> s_listenersInFlight;
402 static std::condition_variable s_listenerFinished;
403 // Nonzero while this thread is inside a listener: a listener that
404 // unregisters (itself or another) must not wait on a call that may be its
405 // own, or one waiting on it.
406 static thread_local int t_inListener = 0;
407
408 1 void Variables::registerListener(const std::string& id, const std::string& name,
409 std::function<void(const std::string&, const std::string&)>&& cb) {
410
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 std::unique_lock<std::mutex> lock(m_lock);
411
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 m_listeners[name][id] = std::move(cb);
412 1 }
413 1 void Variables::unregisterListener(const std::string& id, const std::string& name) {
414
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 std::unique_lock<std::mutex> lock(m_lock);
415
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 auto it = m_listeners.find(name);
416
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (it != m_listeners.end()) {
417
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 it->second.erase(id);
418 }
419
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (t_inListener == 0) {
420
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 auto key = std::make_pair(name, id);
421
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
3 s_listenerFinished.wait(lock, [&key]() { return s_listenersInFlight.count(key) == 0; });
422 1 }
423 1 }
424 4920 void Variables::fireListeners(const std::string& name, const std::string& value) {
425 using CallList = std::vector<std::pair<std::string, std::function<void(const std::string&, const std::string&)>>>;
426 4920 CallList cbs;
427 {
428
1/2
✓ Branch 0 taken 4920 times.
✗ Branch 1 not taken.
4920 std::unique_lock<std::mutex> lock(m_lock);
429
1/2
✓ Branch 0 taken 4920 times.
✗ Branch 1 not taken.
4920 auto it = m_listeners.find(name);
430
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 4919 times.
4920 if (it != m_listeners.end()) {
431
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
2 for (auto const& kv : it->second) {
432
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 cbs.emplace_back(kv.first, kv.second);
433
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 ++s_listenersInFlight[std::make_pair(name, kv.first)];
434 }
435 }
436 4920 }
437
2/2
✓ Branch 0 taken 4919 times.
✓ Branch 1 taken 1 times.
4920 if (cbs.empty()) {
438 4919 return;
439 }
440 // Retires every call counted above when this returns or throws.
441 struct InFlight {
442 Variables* self;
443 const std::string& name;
444 const CallList& calls;
445 1 ~InFlight() {
446 1 --t_inListener;
447 1 std::unique_lock<std::mutex> lock(self->m_lock);
448
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
2 for (auto const& c : calls) {
449 1 auto f = s_listenersInFlight.find(std::make_pair(name, c.first));
450
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 if (f != s_listenersInFlight.end() && --f->second <= 0) {
451 1 s_listenersInFlight.erase(f);
452 }
453 }
454 1 s_listenerFinished.notify_all();
455 1 }
456 };
457 1 ++t_inListener;
458 1 InFlight inFlight{ this, name, cbs };
459
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
2 for (auto& cb : cbs) {
460
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 cb.second(name, value);
461 }
462
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 4919 times.
4920 }
463
464 1745 std::set<std::string> Variables::getReferencedNamesInConfig() {
465 1745 std::set<std::string> referenced;
466 // The presets as CommandManager loaded them, not the file: this read the
467 // file again and indexed whatever it held, so one entry of the wrong shape
468 // (a hand edit, an upload) threw, and GET /api/variables answered 500.
469
1/2
✓ Branch 0 taken 1745 times.
✗ Branch 1 not taken.
1745 Json::Value presets = CommandManager::INSTANCE.GetPresets();
470
3/4
✓ Branch 0 taken 1745 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 191 times.
✓ Branch 3 taken 1745 times.
1936 for (auto const& presetName : presets.getMemberNames()) {
471
8/14
✓ Branch 0 taken 191 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 191 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 191 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 210 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 210 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 401 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 210 times.
✓ Branch 13 taken 191 times.
401 for (auto const& cmd : presets[presetName]) {
472
2/4
✓ Branch 0 taken 210 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 210 times.
✗ Branch 3 not taken.
210 std::string commandName = cmd["command"].asString();
473
1/2
✓ Branch 0 taken 210 times.
✗ Branch 1 not taken.
210 const Json::Value& args = cmd["args"];
474
3/4
✓ Branch 0 taken 1511 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1301 times.
✓ Branch 3 taken 210 times.
1511 for (Json::ArrayIndex i = 0; i < args.size(); i++) {
475
3/6
✓ Branch 0 taken 1301 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1301 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 1301 times.
1301 if (args[i].isNull()) {
476 ✗ continue;
477 }
478 // An argument can be any JSON value. A list argument stored
479 // as JSON (an If's commands) is searched as its text.
480
3/6
✓ Branch 0 taken 1301 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1301 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1301 times.
✗ Branch 5 not taken.
2602 std::string arg = args[i].isConvertibleTo(Json::stringValue) ? args[i].asString()
481
4/18
✓ Branch 0 taken 1301 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1301 times.
✗ 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 taken 1301 times.
✗ Branch 12 not taken.
✓ Branch 13 taken 1301 times.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
2602 : SaveJsonToString(args[i]);
482 // "Set Variable" writes to whatever's in its first arg.
483
8/10
✓ Branch 0 taken 1301 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1298 times.
✓ Branch 3 taken 3 times.
✓ Branch 4 taken 203 times.
✓ Branch 5 taken 1095 times.
✓ Branch 6 taken 203 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 203 times.
✓ Branch 9 taken 1098 times.
1301 if (commandName == "Set Variable" && i == 0 && !arg.empty()) {
484
1/2
✓ Branch 0 taken 203 times.
✗ Branch 1 not taken.
203 referenced.insert(arg);
485 }
486 // %VAR:name% substitution reference, anywhere in any arg.
487 1301 std::size_t pos = 0;
488
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1301 times.
1301 while ((pos = arg.find("%VAR:", pos)) != std::string::npos) {
489 ✗ std::size_t end = arg.find('%', pos + 5);
490 ✗ if (end == std::string::npos) {
491 ✗ break;
492 }
493 ✗ referenced.insert(arg.substr(pos + 5, end - pos - 5));
494 ✗ pos = end + 1;
495 }
496 1301 }
497 210 }
498 1745 }
499 1745 return referenced;
500 1745 }
501
502 // Values at or under this size are reported inline in the bulk listing
503 // endpoints below. Bigger ones are reported as a truncated preview plus
504 // "truncated"/"size" so the listing page's default (non-View) load stays
505 // small even when a Variable holds a full fetched web page or similar -
506 // the full value is still one GET /api/variables/{name} away. Kept small
507 // (not just "under some KB") since these values also render directly in a
508 // table cell - variables.php additionally CSS-truncates the cell itself,
509 // but there's no point shipping bytes the row can't usefully show anyway.
510 static const size_t kInlineValueMaxBytes = 200;
511
512 684760 static void reportValueField(Json::Value& entry, const std::string& value) {
513
1/2
✓ Branch 0 taken 684760 times.
✗ Branch 1 not taken.
684760 if (value.size() <= kInlineValueMaxBytes) {
514
2/4
✓ Branch 0 taken 684760 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 684760 times.
✗ Branch 3 not taken.
684760 entry["value"] = value;
515
2/4
✓ Branch 0 taken 684760 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 684760 times.
✗ Branch 3 not taken.
684760 entry["truncated"] = false;
516 } else {
517 ✗ entry["value"] = value.substr(0, kInlineValueMaxBytes);
518 ✗ entry["truncated"] = true;
519 ✗ entry["size"] = (Json::UInt64)value.size();
520 }
521 684760 }
522
523 1745 void Variables::reportVariables(Json::Value& root) {
524
1/2
✓ Branch 0 taken 1745 times.
✗ Branch 1 not taken.
1745 std::set<std::string> referenced = getReferencedNamesInConfig();
525
1/2
✓ Branch 0 taken 1745 times.
✗ Branch 1 not taken.
1745 std::unique_lock<std::mutex> lock(m_lock);
526
2/2
✓ Branch 0 taken 1723 times.
✓ Branch 1 taken 1745 times.
3468 for (auto const& kv : m_variables) {
527
2/4
✓ Branch 0 taken 1723 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1723 times.
✗ Branch 3 not taken.
1723 reportValueField(root[kv.first], kv.second.value);
528
3/6
✓ Branch 0 taken 1723 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1723 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1723 times.
✗ Branch 5 not taken.
1723 root[kv.first]["persist"] = kv.second.persist;
529
3/6
✓ Branch 0 taken 1723 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1723 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1723 times.
✗ Branch 5 not taken.
1723 root[kv.first]["lastUpdated"] = (Json::Int64)kv.second.lastUpdated;
530
4/8
✓ Branch 0 taken 1723 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1723 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1723 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1723 times.
✗ Branch 7 not taken.
1723 root[kv.first]["used"] = referenced.count(kv.first) > 0;
531 }
532 1745 }
533
534 // fpp_* status variables are computed fresh from live Player/system state on
535 // every call (ComputeFppStatusVariables() above) - there's no persisted
536 // "last updated" the way User/MQTT Variables have. Approximate one by
537 // tracking each name's last-seen value here and only stamping a new time
538 // when it actually changes, scoped to this bulk-listing report so it doesn't
539 // add tracking overhead to the getVariable()/If-condition hot path. The
540 // first observation after an fppd (re)start always stamps "now" - there's no
541 // way to know when a value last changed before we started watching it.
542 static std::mutex fppVarChangeLock;
543 static std::map<std::string, std::pair<std::string, time_t>> fppVarLastChange;
544
545 25280 void Variables::reportFppVariables(Json::Value& root) {
546 // Computed before taking fppVarChangeLock, never under it: computing them
547 // takes the playlist's lock (Player::GetCurrentStatus), and the main loop
548 // holds that lock while a playlist's If / Set Variable command entry runs
549 // and lists the variables. Holding this lock across it was the other
550 // half of a lock-order deadlock that hung fppd.
551
1/2
✓ Branch 0 taken 25280 times.
✗ Branch 1 not taken.
25280 auto vars = ComputeFppStatusVariables();
552
1/2
✓ Branch 0 taken 25280 times.
✗ Branch 1 not taken.
25280 std::unique_lock<std::mutex> lock(fppVarChangeLock);
553 25280 time_t now = time(nullptr);
554
2/2
✓ Branch 0 taken 682560 times.
✓ Branch 1 taken 25280 times.
707840 for (auto const& kv : vars) {
555
1/2
✓ Branch 0 taken 682560 times.
✗ Branch 1 not taken.
682560 auto it = fppVarLastChange.find(kv.first);
556
6/6
✓ Branch 0 taken 682533 times.
✓ Branch 1 taken 27 times.
✓ Branch 2 taken 930 times.
✓ Branch 3 taken 681603 times.
✓ Branch 4 taken 957 times.
✓ Branch 5 taken 681603 times.
682560 if (it == fppVarLastChange.end() || it->second.first != kv.second) {
557
2/4
✓ Branch 0 taken 957 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 957 times.
✗ Branch 3 not taken.
957 fppVarLastChange[kv.first] = { kv.second, now };
558 }
559
2/4
✓ Branch 0 taken 682560 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 682560 times.
✗ Branch 3 not taken.
682560 reportValueField(root[kv.first], kv.second);
560
4/8
✓ Branch 0 taken 682560 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 682560 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 682560 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 682560 times.
✗ Branch 7 not taken.
682560 root[kv.first]["lastUpdated"] = (Json::Int64)fppVarLastChange[kv.first].second;
561 }
562 25280 }
563
564 7227 void Variables::reportMqttVariables(Json::Value& root) {
565
2/2
✓ Branch 0 taken 7059 times.
✓ Branch 1 taken 168 times.
7227 if (!mqtt) {
566 7059 return;
567 }
568
1/2
✓ Branch 0 taken 168 times.
✗ Branch 1 not taken.
168 Json::Value cache;
569
1/2
✓ Branch 0 taken 168 times.
✗ Branch 1 not taken.
168 mqtt->dumpMessageCache(cache, true);
570
3/4
✓ Branch 0 taken 168 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 477 times.
✓ Branch 3 taken 168 times.
645 for (auto const& topic : cache.getMemberNames()) {
571
1/2
✓ Branch 0 taken 477 times.
✗ Branch 1 not taken.
477 std::string name = "mqtt-" + topic;
572
5/10
✓ Branch 0 taken 477 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 477 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 477 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 477 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 477 times.
✗ Branch 9 not taken.
477 reportValueField(root[name], cache[topic]["value"].asString());
573
5/10
✓ Branch 0 taken 477 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 477 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 477 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 477 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 477 times.
✗ Branch 9 not taken.
477 root[name]["lastUpdated"] = cache[topic]["lastUpdated"];
574 645 }
575 168 }
576
577 // --------------------------------------------------------------------------
578 // OpenAPI docs for the /variables/* endpoints handled below.
579 // --------------------------------------------------------------------------
580
581 /**
582 * List all User Variables and their current values. Pass ?validateExpression
583 * instead to syntax-check an expression (for the Set Variable "Expression"
584 * field) against currently-known variables, without setting anything. Pass
585 * ?fpp=true instead to list the read-only "fpp_" status variables (current
586 * playlist/sequence, play state, volume, etc.) instead of User Variables.
587 *
588 * @route GET /api/variables
589 * @param string validateExpression If set, validate this expression instead of listing variables.
590 * @param boolean conditionExpr If "true" alongside validateExpression, classify it the way the If
591 * Check editor's Name/Value fields actually evaluate it (exact variable match / formula / inert
592 * literal text) instead of a bare compile check.
593 * @param boolean fpp If "true", list the read-only fpp- status variables instead of User Variables.
594 * @param boolean mqtt If "true", list the read-only mqtt-<topic> variables (MQTT's own last-message-
595 * per-topic cache) instead of User Variables.
596 * @response 200 Object keyed by variable name, each with `value`, `truncated`,
597 * `persist`, `lastUpdated` (unix timestamp) and `used` (true if referenced
598 * anywhere in config/commandPresets.json, either as a Set Variable target or
599 * via %VAR:name%). If `validateExpression` was passed, `{"valid": true|false,
600 * "kind": "formula"|"template"|"literal"}` (or, with `conditionExpr=true`,
601 * `{"valid": true|false, "kind": "variable"|"formula"|"literal"}`)
602 * instead. If `fpp=true`, each entry has `value`/`truncated`/`lastUpdated`
603 * (no persist/used - these are live-computed, not stored; `lastUpdated` is
604 * approximated as the last time this endpoint observed the value actually
605 * change, not a true change time, and resets on an fppd restart). If
606 * `mqtt=true`, each entry has `value`/`truncated`/`lastUpdated` (the time
607 * the topic's last MQTT message was received; no persist/used). Values over
608 * 200 bytes are reported truncated (`truncated: true`, plus `size` = the
609 * real byte count) to keep this listing endpoint cheap - fetch GET
610 * /api/variables/{name} for the full value.
611 */
612
613 /**
614 * Read a single User Variable's current value.
615 *
616 * @route GET /api/variables/{name}
617 * @response 200 The variable's value as plain text (empty string if unset).
618 */
619 // getPathPieces() splits the whole path on '/', so a name containing '/'
620 // (e.g. an mqtt-<topic> Variable) lands across multiple pieces instead of
621 // just parts[1]. Rejoin everything after the resource segment (parts[0],
622 // "variables") back into one name.
623 865 static std::string joinNameFromPathPieces(const std::vector<std::string>& parts) {
624 865 std::string name;
625
2/2
✓ Branch 0 taken 1040 times.
✓ Branch 1 taken 865 times.
1905 for (size_t i = 1; i < parts.size(); ++i) {
626
2/2
✓ Branch 0 taken 175 times.
✓ Branch 1 taken 865 times.
1040 if (i > 1) {
627
1/2
✓ Branch 0 taken 175 times.
✗ Branch 1 not taken.
175 name += "/";
628 }
629
1/2
✓ Branch 0 taken 1040 times.
✗ Branch 1 not taken.
1040 name += parts[i];
630 }
631 865 return name;
632 ✗ }
633
634 27393 HttpResponsePtr Variables::render_GET(const HttpRequestPtr& req) {
635
2/4
✓ Branch 0 taken 27393 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 27393 times.
✗ Branch 3 not taken.
27393 auto parts = getPathPieces(req->path());
636 27393 int plen = parts.size();
637
638 // ?validateExpression=<expr> - live syntax check for the Set Variable
639 // "Expression" field, without setting anything. Binds every currently
640 // known Variable (User Variables plus the read-only fpp-/mqtt- ones)
641 // first, same as SetVariableCommand::run(), so a reference to an
642 // existing variable name validates correctly and a reference to a
643 // nonexistent one is correctly flagged invalid.
644
2/4
✓ Branch 0 taken 27393 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 27393 times.
✗ Branch 3 not taken.
27393 std::string exprArg = getRequestArg(req, "validateExpression");
645
2/2
✓ Branch 0 taken 13 times.
✓ Branch 1 taken 27380 times.
27393 if (!exprArg.empty()) {
646
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 Json::Value result;
647 // conditionExpr=true: the If Check editor's Name/Value fields, which
648 // go through ConditionNode::ClassifyNameOrExpression's auto-detect at
649 // runtime (Condition.cpp's EvaluateNameOrExpression) rather than a
650 // bare ExpressionProcessor::compile() - a raw compile() call always
651 // "succeeds" on ordinary unmatched text (it's just treated as inert
652 // literal, per that function's own documented behavior), so it can't
653 // tell a real variable match, an actual formula, and a typo'd/
654 // unmatched name apart the way the Check editor's icon needs to.
655
5/8
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 13 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 7 times.
✓ Branch 7 taken 6 times.
26 if (getRequestArg(req, "conditionExpr") == "true") {
656 7 bool compileOk = true;
657
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 auto kind = ConditionNode::ClassifyNameOrExpression(exprArg, compileOk);
658
2/4
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
7 result["valid"] = compileOk;
659
6/8
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 7 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 7 times.
✗ Branch 7 not taken.
7 result["kind"] = kind == ConditionNode::ExpressionKind::Variable ? "variable" : (kind == ConditionNode::ExpressionKind::Formula ? "formula" : "literal");
660 } else {
661 // Set Variable's "Expression" field (VariableCommands.cpp's
662 // SetVariableCommand::run(), type=="Expression" branch) really is
663 // just a bare ExpressionProcessor::compile() call with no
664 // auto-detect - so validating it the same way here is correct,
665 // not a shortcut.
666
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 ExpressionProcessor proc;
667 6 std::map<std::string, ExpressionProcessor::ExpressionVariable> boundVars;
668
3/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 168 times.
✓ Branch 3 taken 6 times.
174 for (auto const& varName : getAllVariableNames()) {
669
1/2
✓ Branch 0 taken 168 times.
✗ Branch 1 not taken.
168 auto inserted = boundVars.try_emplace(varName, varName);
670
3/6
✓ Branch 0 taken 168 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 168 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 168 times.
✗ Branch 5 not taken.
336 inserted.first->second.setValue(getVariable(varName));
671
1/2
✓ Branch 0 taken 168 times.
✗ Branch 1 not taken.
168 proc.bindVariable(&inserted.first->second);
672 6 }
673
3/6
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 6 times.
✗ Branch 5 not taken.
6 result["valid"] = proc.compile(exprArg);
674 // ExpressionProcessor::compile() treats anything not starting with
675 // '=' (and with no embedded "==...=="/"%%...%%" markers) as inert
676 // literal text to store verbatim - it always "compiles" fine, so a
677 // bare variable name like "outsideTemp" (meant to reference that
678 // variable's value) silently becomes the literal string
679 // "outsideTemp" instead. A plain valid:true can't distinguish that
680 // mistake from an intentional formula, so flag it here for the
681 // frontend to warn about instead of showing a bare green check.
682
4/6
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 4 times.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
6 bool looksLikeFormula = exprArg.size() > 1 && exprArg[0] == '=' && exprArg[1] != '=';
683
3/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 4 times.
6 bool hasEmbeddedMarker = exprArg.find("==") != std::string::npos || exprArg.find("%%") != std::string::npos;
684
6/8
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 6 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 6 times.
✗ Branch 7 not taken.
6 result["kind"] = looksLikeFormula ? "formula" : (hasEmbeddedMarker ? "template" : "literal");
685 6 }
686
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
26 std::string resultStr = SaveJsonToString(result);
687
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
13 return makeStringResponse(resultStr, 200, "application/json");
688 13 }
689
690
2/2
✓ Branch 0 taken 355 times.
✓ Branch 1 taken 27025 times.
27380 if (plen > 1) {
691
1/2
✓ Branch 0 taken 355 times.
✗ Branch 1 not taken.
355 std::string name = joinNameFromPathPieces(parts);
692
2/4
✓ Branch 0 taken 355 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 355 times.
✗ Branch 3 not taken.
710 std::string v = getVariable(name, "");
693
2/4
✓ Branch 0 taken 355 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 355 times.
✗ Branch 3 not taken.
355 return makeStringResponse(v, 200, "text/plain");
694 355 }
695
696
1/2
✓ Branch 0 taken 27025 times.
✗ Branch 1 not taken.
27025 Json::Value result;
697
5/8
✓ Branch 0 taken 27025 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 27025 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 27025 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 25280 times.
✓ Branch 7 taken 1745 times.
54050 if (getRequestArg(req, "fpp") == "true") {
698
1/2
✓ Branch 0 taken 25280 times.
✗ Branch 1 not taken.
25280 reportFppVariables(result);
699
4/8
✓ Branch 0 taken 1745 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1745 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1745 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 1745 times.
3490 } else if (getRequestArg(req, "mqtt") == "true") {
700 ✗ reportMqttVariables(result);
701 } else {
702
1/2
✓ Branch 0 taken 1745 times.
✗ Branch 1 not taken.
1745 reportVariables(result);
703 }
704
2/4
✓ Branch 0 taken 27025 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 27025 times.
✗ Branch 3 not taken.
54050 std::string resultStr = SaveJsonToString(result, " ");
705
2/4
✓ Branch 0 taken 27025 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 27025 times.
✗ Branch 3 not taken.
27025 return makeStringResponse(resultStr, 200, "application/json");
706 27393 }
707
708 /**
709 * Set a User Variable's value. Body is the raw new value (plain text, not JSON).
710 * Add ?persist=true to save it to config/variables.json so it survives an
711 * fppd restart; omitted or any other value means the variable is in-memory only.
712 *
713 * @route POST /api/variables/{name}
714 * @param boolean persist Persist the value to disk so it survives a restart.
715 * @body new-value-goes-here
716 * @response 200 "OK"
717 * @response 400 Missing variable name in the path.
718 */
719 270 HttpResponsePtr Variables::render_POST(const HttpRequestPtr& req) {
720
2/4
✓ Branch 0 taken 270 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 270 times.
✗ Branch 3 not taken.
270 auto parts = getPathPieces(req->path());
721 270 int plen = parts.size();
722
723
2/2
✓ Branch 0 taken 255 times.
✓ Branch 1 taken 15 times.
270 if (plen > 1) {
724
1/2
✓ Branch 0 taken 255 times.
✗ Branch 1 not taken.
255 std::string name = joinNameFromPathPieces(parts);
725
2/4
✓ Branch 0 taken 255 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 255 times.
255 if (IsFppStatusVariableName(name)) {
726 ✗ return makeStringResponse("\"" + name + "\" is a read-only fpp- status variable and cannot be set", 400, "text/plain");
727 }
728
1/2
✓ Branch 0 taken 255 times.
✗ Branch 1 not taken.
255 std::string value = std::string(getRequestContent(req));
729
3/6
✓ Branch 0 taken 255 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 255 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 255 times.
✗ Branch 5 not taken.
255 bool persist = getRequestArg(req, "persist") == "true";
730
4/8
✗ Branch 0 not taken.
✓ Branch 1 taken 255 times.
✓ Branch 2 taken 255 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 255 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 255 times.
✗ Branch 7 not taken.
255 LogDebug(VB_COMMAND, "POST /api/variables/%s from %s: \"%s\"%s\n", name.c_str(),
731 getEffectiveClientIP(req).c_str(), TruncateForLog(value).c_str(), persist ? " (persist)" : "");
732
1/2
✓ Branch 0 taken 255 times.
✗ Branch 1 not taken.
255 setVariable(name, value, persist);
733
3/6
✓ Branch 0 taken 255 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 255 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 255 times.
✗ Branch 5 not taken.
765 return makeStringResponse("OK", 200, "text/plain");
734 255 }
735
3/6
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 15 times.
✗ Branch 5 not taken.
60 return makeStringResponse("Bad Request", 400, "text/plain");
736 270 }
737
738 /**
739 * Delete a User Variable entirely - unlike POSTing an empty body (the UI's
740 * "Clear"), which only resets its value/persist flag and leaves the row
741 * behind, this removes the name from the list altogether.
742 *
743 * @route DELETE /api/variables/{name}
744 * @response 200 "OK"
745 * @response 400 Missing variable name in the path, or name is a read-only
746 * fpp-/mqtt- variable (nothing to delete - those aren't stored).
747 */
748 270 HttpResponsePtr Variables::render_DELETE(const HttpRequestPtr& req) {
749
2/4
✓ Branch 0 taken 270 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 270 times.
✗ Branch 3 not taken.
270 auto parts = getPathPieces(req->path());
750
2/2
✓ Branch 0 taken 15 times.
✓ Branch 1 taken 255 times.
270 if (parts.size() <= 1) {
751
3/6
✓ Branch 0 taken 15 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 15 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 15 times.
✗ Branch 5 not taken.
60 return makeStringResponse("Bad Request", 400, "text/plain");
752 }
753
1/2
✓ Branch 0 taken 255 times.
✗ Branch 1 not taken.
255 std::string name = joinNameFromPathPieces(parts);
754
5/10
✓ Branch 0 taken 255 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 255 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 255 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 255 times.
✗ Branch 8 not taken.
✓ Branch 9 taken 255 times.
255 if (IsFppStatusVariableName(name) || IsMqttVariableName(name)) {
755 ✗ return makeStringResponse("\"" + name + "\" is read-only and cannot be deleted", 400, "text/plain");
756 }
757
2/4
✓ Branch 0 taken 255 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 255 times.
✗ Branch 3 not taken.
255 LogDebug(VB_COMMAND, "DELETE /api/variables/%s from %s\n", name.c_str(), getEffectiveClientIP(req).c_str());
758
1/2
✓ Branch 0 taken 255 times.
✗ Branch 1 not taken.
255 deleteVariable(name);
759
3/6
✓ Branch 0 taken 255 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 255 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 255 times.
✗ Branch 5 not taken.
1020 return makeStringResponse("OK", 200, "text/plain");
760 270 }
761