GCC Code Coverage Report


Directory: ./
File: commands/Commands.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 727 819 88.8%
Functions: 68 70 97.1%
Branches: 1200 2432 49.3%

Line Branch Exec Source
1 /*
2 * This file is part of the Falcon Player (FPP) and is Copyright (C)
3 * 2013-2022 by the Falcon Player Developers.
4 *
5 * The Falcon Player (FPP) is free software, and is covered under
6 * multiple Open Source licenses. Please see the included 'LICENSES'
7 * file for descriptions of what files are covered by each license.
8 *
9 * This source file is covered under the LGPL v2.1 as described in the
10 * included LICENSE.LGPL file.
11 */
12
13 #include "fpp-pch.h"
14
15 #include "fpp-json.h"
16 #include "fpphttp.h" // drogon/HTTP helpers used here; no longer pulled transitively (see fpphttp_types.h)
17 #include "fpphttp_clientip.h" // getEffectiveClientIP() - FPP-internal, not in the plugin API
18
19 #include <algorithm>
20 #include <thread>
21
22 #include <sys/stat.h>
23
24 #include "../Variables.h"
25 #include "../common.h"
26 #include "../log.h"
27 #include "../Warnings.h"
28
29 #include "../Events.h"
30
31 #include "EventCommands.h"
32 #include "FileMonitor.h"
33 #include "IfCommand.h"
34 #include "MediaCommands.h"
35 #include "MultiSync.h"
36 #include "PlaylistCommands.h"
37 #include "VariableCommands.h"
38
39 // %VAR:name% - substitute a named User Variable's current value. Applied
40 // once, centrally, inside CommandManager::run() (the one chokepoint every
41 // caller funnels through - api/command, presets, GPIO, MultiSync, playlist
42 // "FPP Command" entries, If, recurring tasks, ...) rather than at each
43 // individual call site, so it works the same way no matter how a command
44 // gets run. Defined further down (needs Variables.h); forward-declared here
45 // since run() is used well above that point in the file.
46 static std::string ReplaceVariableKeywords(std::string str);
47 static void ResolveCommandArgs(const Command* cmd, std::vector<std::string>& args);
48
49 CommandManager CommandManager::INSTANCE;
50
51 // Out-of-line storage for anything added to Command/CommandArg after plugin API
52 // version 5. Both hold it by unique_ptr, so growing either struct here is
53 // invisible to a plugin's sizeof and cannot desync a std::list node or shift a
54 // subclass's members. See the LAYOUT RULE comments in Commands.h.
55 struct Command::Data {
56 // (empty - first field added after API version 5 goes here)
57 };
58 struct Command::CommandArg::Ext {
59 bool advanced = false;
60 std::string help;
61 std::string section;
62 std::map<std::string, std::vector<std::string>> children;
63 bool toggleStyle = false;
64 std::string toggleLabel;
65 };
66
67 583213 Command::Command(const std::string& n) :
68 583213 name(n),
69
1/2
✓ Branch 0 taken 583213 times.
✗ Branch 1 not taken.
1749639 description("") {
70 583213 }
71 54766 Command::Command(const std::string& n, const std::string& descript) :
72 54766 name(n),
73
1/2
✓ Branch 0 taken 54766 times.
✗ Branch 1 not taken.
54766 description(descript) {
74 54766 }
75 435104 Command::~Command() {}
76
77 4537184 Command::CommandArg::CommandArg(const std::string& n, const std::string& t, const std::string& d, bool o) :
78 4537184 name(n),
79
1/2
✓ Branch 0 taken 4537184 times.
✗ Branch 1 not taken.
4537184 type(t),
80
1/2
✓ Branch 0 taken 4537184 times.
✗ Branch 1 not taken.
4537184 description(d),
81 4537184 optional(o),
82 4537184 min(-1),
83 4537184 max(-1),
84 4537184 allowBlanks(false),
85 4537184 adjustable(false) {
86 4537184 }
87 4515422 Command::CommandArg::CommandArg(const CommandArg& o) :
88 4515422 name(o.name),
89
1/2
✓ Branch 0 taken 4515422 times.
✗ Branch 1 not taken.
4515422 type(o.type),
90
1/2
✓ Branch 0 taken 4515422 times.
✗ Branch 1 not taken.
4515422 description(o.description),
91 4515422 optional(o.optional),
92
1/2
✓ Branch 0 taken 4515422 times.
✗ Branch 1 not taken.
4515422 contentListUrl(o.contentListUrl),
93
1/2
✓ Branch 0 taken 4515422 times.
✗ Branch 1 not taken.
4515422 contentList(o.contentList),
94 4515422 allowBlanks(o.allowBlanks),
95 4515422 min(o.min),
96 4515422 max(o.max),
97
1/2
✓ Branch 0 taken 4515422 times.
✗ Branch 1 not taken.
4515422 defaultValue(o.defaultValue),
98
1/2
✓ Branch 0 taken 4515422 times.
✗ Branch 1 not taken.
4515422 adjustableGetValueURL(o.adjustableGetValueURL),
99 4515422 adjustable(o.adjustable),
100 // Deep copy, not a shared handle: the builder pattern returns CommandArg&,
101 // so nearly every args.push_back() in the tree copies from an lvalue and
102 // the temporary is then destroyed. Aliasing ext would leave a dangling one.
103
3/4
✓ Branch 0 taken 58416 times.
✓ Branch 1 taken 4457006 times.
✓ Branch 2 taken 58416 times.
✗ Branch 3 not taken.
4515422 ext(o.ext ? std::make_unique<Ext>(*o.ext) : nullptr) {
104 4515422 }
105
2/4
✓ Branch 0 taken 21762 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 21762 times.
✗ Branch 3 not taken.
21762 Command::CommandArg::CommandArg(CommandArg&& o) noexcept = default;
106 5599885 Command::CommandArg::~CommandArg() = default;
107
108 // Ext is allocated on first write; readers fall back to a default so an arg that
109 // set none of these still costs nothing. Every accessor pair below follows this
110 // shape - copy it when adding a field.
111 87138 Command::CommandArg::Ext& Command::CommandArg::mutableExt() {
112
2/2
✓ Branch 0 taken 58416 times.
✓ Branch 1 taken 28722 times.
87138 if (!ext) {
113
1/2
✓ Branch 0 taken 58416 times.
✗ Branch 1 not taken.
58416 ext = std::make_unique<Ext>();
114 }
115 87138 return *ext;
116 }
117
118 26320 Command::CommandArg& Command::CommandArg::setSection(const std::string& s) {
119 26320 mutableExt().section = s;
120 26320 return *this;
121 }
122 28604 const std::string& Command::CommandArg::getSection() const {
123
3/4
✓ Branch 0 taken 127 times.
✓ Branch 1 taken 28477 times.
✓ Branch 2 taken 127 times.
✗ Branch 3 not taken.
28604 static const std::string EMPTY;
124
2/2
✓ Branch 0 taken 2268 times.
✓ Branch 1 taken 26336 times.
28604 return ext ? ext->section : EMPTY;
125 }
126
127 49032 Command::CommandArg& Command::CommandArg::setHelp(const std::string& h) {
128 49032 mutableExt().help = h;
129 49032 return *this;
130 }
131 29432 const std::string& Command::CommandArg::getHelp() const {
132
3/4
✓ Branch 0 taken 127 times.
✓ Branch 1 taken 29305 times.
✓ Branch 2 taken 127 times.
✗ Branch 3 not taken.
29432 static const std::string EMPTY;
133
2/2
✓ Branch 0 taken 3096 times.
✓ Branch 1 taken 26336 times.
29432 return ext ? ext->help : EMPTY;
134 }
135
136 ✗ Command::CommandArg& Command::CommandArg::setAdvanced(bool a) {
137 ✗ mutableExt().advanced = a;
138 ✗ return *this;
139 }
140 27980 bool Command::CommandArg::isAdvanced() const {
141
3/4
✓ Branch 0 taken 1644 times.
✓ Branch 1 taken 26336 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 1644 times.
27980 return ext && ext->advanced;
142 }
143
144 4234 Command::CommandArg& Command::CommandArg::setChildren(std::map<std::string, std::vector<std::string>> c) {
145 4234 mutableExt().children = std::move(c);
146 4234 return *this;
147 }
148 28088 const std::map<std::string, std::vector<std::string>>& Command::CommandArg::getChildren() const {
149
3/4
✓ Branch 0 taken 127 times.
✓ Branch 1 taken 27961 times.
✓ Branch 2 taken 127 times.
✗ Branch 3 not taken.
28088 static const std::map<std::string, std::vector<std::string>> EMPTY;
150
2/2
✓ Branch 0 taken 1752 times.
✓ Branch 1 taken 26336 times.
28088 return ext ? ext->children : EMPTY;
151 }
152
153 3776 Command::CommandArg& Command::CommandArg::setToggleStyle(bool t) {
154 3776 mutableExt().toggleStyle = t;
155 3776 return *this;
156 }
157 27980 bool Command::CommandArg::isToggleStyle() const {
158
4/4
✓ Branch 0 taken 1644 times.
✓ Branch 1 taken 26336 times.
✓ Branch 2 taken 120 times.
✓ Branch 3 taken 1524 times.
27980 return ext && ext->toggleStyle;
159 }
160
161 3776 Command::CommandArg& Command::CommandArg::setToggleLabel(const std::string& l) {
162 3776 mutableExt().toggleLabel = l;
163 3776 return *this;
164 }
165 28100 const std::string& Command::CommandArg::getToggleLabel() const {
166
3/4
✓ Branch 0 taken 127 times.
✓ Branch 1 taken 27973 times.
✓ Branch 2 taken 127 times.
✗ Branch 3 not taken.
28100 static const std::string EMPTY;
167
2/2
✓ Branch 0 taken 1764 times.
✓ Branch 1 taken 26336 times.
28100 return ext ? ext->toggleLabel : EMPTY;
168 }
169
170 6472 Json::Value Command::getDescription() {
171 6472 Json::Value cmd;
172
2/4
✓ Branch 0 taken 6472 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6472 times.
✗ Branch 3 not taken.
6472 cmd["name"] = name;
173
2/2
✓ Branch 0 taken 1751 times.
✓ Branch 1 taken 4721 times.
6472 if (!description.empty()) {
174
2/4
✓ Branch 0 taken 1751 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1751 times.
✗ Branch 3 not taken.
1751 cmd["description"] = description;
175 }
176
2/2
✓ Branch 0 taken 27980 times.
✓ Branch 1 taken 6472 times.
34452 for (auto& ar : args) {
177
1/2
✓ Branch 0 taken 27980 times.
✗ Branch 1 not taken.
27980 Json::Value a;
178
2/4
✓ Branch 0 taken 27980 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 27980 times.
✗ Branch 3 not taken.
27980 a["name"] = ar.name;
179
2/4
✓ Branch 0 taken 27980 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 27980 times.
✗ Branch 3 not taken.
27980 a["type"] = ar.type;
180
2/4
✓ Branch 0 taken 27980 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 27980 times.
✗ Branch 3 not taken.
27980 a["description"] = ar.description;
181
2/4
✓ Branch 0 taken 27980 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 27980 times.
✗ Branch 3 not taken.
27980 a["optional"] = ar.optional;
182
183
2/2
✓ Branch 0 taken 10481 times.
✓ Branch 1 taken 17499 times.
27980 if (ar.min != ar.max) {
184
2/4
✓ Branch 0 taken 10481 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 10481 times.
✗ Branch 3 not taken.
10481 a["min"] = ar.min;
185
2/4
✓ Branch 0 taken 10481 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 10481 times.
✗ Branch 3 not taken.
10481 a["max"] = ar.max;
186 }
187
2/2
✓ Branch 0 taken 2109 times.
✓ Branch 1 taken 25871 times.
27980 if (!ar.contentListUrl.empty()) {
188
2/4
✓ Branch 0 taken 2109 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2109 times.
✗ Branch 3 not taken.
2109 a["contentListUrl"] = ar.contentListUrl;
189
2/4
✓ Branch 0 taken 2109 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2109 times.
✗ Branch 3 not taken.
2109 a["allowBlanks"] = ar.allowBlanks;
190
2/2
✓ Branch 0 taken 5126 times.
✓ Branch 1 taken 20745 times.
25871 } else if (!ar.contentList.empty()) {
191
2/2
✓ Branch 0 taken 173189 times.
✓ Branch 1 taken 5126 times.
178315 for (auto& x : ar.contentList) {
192
3/6
✓ Branch 0 taken 173189 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 173189 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 173189 times.
✗ Branch 5 not taken.
173189 a["contents"].append(x);
193 }
194 }
195
3/4
✓ Branch 0 taken 27980 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 21339 times.
✓ Branch 3 taken 6641 times.
27980 if (ar.defaultValue != "") {
196
2/4
✓ Branch 0 taken 21339 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 21339 times.
✗ Branch 3 not taken.
21339 a["default"] = ar.defaultValue;
197 }
198
2/2
✓ Branch 0 taken 309 times.
✓ Branch 1 taken 27671 times.
27980 if (ar.adjustable) {
199
2/4
✓ Branch 0 taken 309 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 309 times.
✗ Branch 3 not taken.
309 a["adjustable"] = true;
200
2/2
✓ Branch 0 taken 60 times.
✓ Branch 1 taken 249 times.
309 if (!ar.adjustableGetValueURL.empty()) {
201
2/4
✓ Branch 0 taken 60 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 60 times.
✗ Branch 3 not taken.
60 a["adjustableGetValueURL"] = ar.adjustableGetValueURL;
202 }
203 }
204
2/4
✓ Branch 0 taken 27980 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 27980 times.
27980 if (ar.isAdvanced()) {
205 ✗ a["advanced"] = true;
206 }
207
3/4
✓ Branch 0 taken 27980 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1452 times.
✓ Branch 3 taken 26528 times.
27980 if (!ar.getHelp().empty()) {
208
3/6
✓ Branch 0 taken 1452 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1452 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1452 times.
✗ Branch 5 not taken.
1452 a["help"] = ar.getHelp();
209 }
210
3/4
✓ Branch 0 taken 27980 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 624 times.
✓ Branch 3 taken 27356 times.
27980 if (!ar.getSection().empty()) {
211
3/6
✓ Branch 0 taken 624 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 624 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 624 times.
✗ Branch 5 not taken.
624 a["section"] = ar.getSection();
212 }
213
3/4
✓ Branch 0 taken 27980 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 120 times.
✓ Branch 3 taken 27860 times.
27980 if (ar.isToggleStyle()) {
214
2/4
✓ Branch 0 taken 120 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 120 times.
✗ Branch 3 not taken.
120 a["toggleStyle"] = true;
215 }
216
3/4
✓ Branch 0 taken 27980 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 120 times.
✓ Branch 3 taken 27860 times.
27980 if (!ar.getToggleLabel().empty()) {
217
3/6
✓ Branch 0 taken 120 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 120 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 120 times.
✗ Branch 5 not taken.
120 a["toggleLabel"] = ar.getToggleLabel();
218 }
219
3/4
✓ Branch 0 taken 27980 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 108 times.
✓ Branch 3 taken 27872 times.
27980 if (!ar.getChildren().empty()) {
220
3/4
✓ Branch 0 taken 108 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 768 times.
✓ Branch 3 taken 108 times.
876 for (auto& kv : ar.getChildren()) {
221
2/2
✓ Branch 0 taken 3660 times.
✓ Branch 1 taken 768 times.
4428 for (auto& childName : kv.second) {
222
4/8
✓ Branch 0 taken 3660 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3660 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3660 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 3660 times.
✗ Branch 7 not taken.
3660 a["children"][kv.first].append(childName);
223 }
224 }
225 }
226
227
2/4
✓ Branch 0 taken 27980 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 27980 times.
✗ Branch 3 not taken.
27980 cmd["args"].append(a);
228 27980 }
229 6472 return cmd;
230 ✗ }
231
232 2346 CommandManager::CommandManager() {
233 2346 }
234
235 1888 void CommandManager::Init() {
236
1/2
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
1888 LoadPresets(true);
237
238
2/4
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
2048 FileMonitor::INSTANCE.AddFile("CommandManager:CommandPresets.json",
239
3/6
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
5664 FPP_DIR_CONFIG("/commandPresets.json"),
240 3776 [this]() {
241 // No presetsMutex here: LoadPresets()
242 // takes it for the swap, and it is not
243 // recursive.
244 10 MaybeReloadPresets();
245 10 });
246
247
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new StopPlaylistCommand(), "Playlist", 0);
248
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new StopGracefullyPlaylistCommand(), "Playlist", 0);
249
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new RestartPlaylistCommand(), "Playlist", 1);
250
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new NextPlaylistCommand(), "Playlist", 0);
251
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new PrevPlaylistCommand(), "Playlist", 0);
252
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new StartPlaylistCommand(), "Playlist", 0);
253
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new TogglePlaylistCommand(), "Playlist", 0);
254
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new StartPlaylistAtCommand(), "Playlist", 1);
255
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new StartPlaylistAtRandomCommand(), "Playlist", 1);
256
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new InsertPlaylistCommand(), "Playlist", 0);
257
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new InsertPlaylistImmediate(), "Playlist", 0);
258
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new InsertRandomItemFromPlaylistCommand(), "Playlist", 1);
259 #ifdef HAS_GSTREAMER
260
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new PlayMediaCommand(), "Media", 0);
261
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new StopMediaCommand(), "Media", 0);
262
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new StopAllMediaCommand(), "Media", 0);
263
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new StopMediaSlotCommand(), "Media", 1);
264
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new SetSlotVolumeCommand(), "Audio", 1);
265
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new MediaSlotStatusCommand(), "Media", 1);
266 #endif
267 #ifdef HAS_AES67_GSTREAMER
268
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new AES67ApplyCommand(), "Audio", 3);
269
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new AES67CleanupCommand(), "Audio", 3);
270
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new AES67TestCommand(), "Audio", 3);
271
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new ApplyRoutingPresetCommand(), "Audio", 3);
272 #endif
273 #ifdef HAS_OPUS_RTP_GSTREAMER
274
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new OpusRTPApplyCommand(), "Audio", 3);
275
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new OpusRTPCleanupCommand(), "Audio", 3);
276 #endif
277
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new PlaylistPauseCommand(), "Playlist", 0);
278
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new PlaylistResumeCommand(), "Playlist", 0);
279
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new SetPlaylistRepeatCommand(), "Playlist", 0);
280
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new TriggerPresetCommand(), "Events", 0);
281
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new TriggerPresetInFutureCommand(), "Events", 1);
282
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new TriggerPresetSlotCommand(), "Events", 1);
283
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new TriggerMultiplePresetsCommand(), "Events", 1);
284
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new TriggerMultiplePresetSlotsCommand(), "Events", 1);
285
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new RunScriptEvent(), "Events", 1);
286
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new StartEffectCommand(), "Effects", 0);
287
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new StartFSEQAsEffectCommand(), "Effects", 0);
288
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new StopFSEQAsEffectCommand(), "Effects", 0);
289
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new StopEffectCommand(), "Effects", 0);
290
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new StopAllEffectsCommand(), "Effects", 0);
291
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new SetVolumeCommand(), "Audio", 0);
292
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new AdjustVolumeCommand(), "Audio", 0);
293
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new IncreaseVolumeCommand(), "Audio", 0);
294
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new DecreaseVolumeCommand(), "Audio", 0);
295
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new URLCommand(), "Events", 1);
296
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new AllLightsOffCommand(), "Effects", 0);
297
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new SwitchToPlayerModeCommand(), "System", 1);
298
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new SwitchToRemoteModeCommand(), "System", 1);
299
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new RestartFPPDCommand(), "System", 1);
300
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new RebootCommand(), "System", 1);
301
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new ShutdownCommand(), "System", 1);
302
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
3936 addCategorizedCommand(new SetVariableCommand(), "Events", 1);
303
5/12
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1888 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1888 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1888 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
2208 addCategorizedCommand(new IfCommand(), "Events", 1);
304
305 std::function<void(const std::string&, const std::string&)> f =
306 1888 [](const std::string& topic, const std::string& payload) {
307 ✗ if (topic.size() <= 13) {
308 ✗ Json::Value val;
309 ✗ bool success = LoadJsonFromString(payload, val);
310 ✗ if (success && val.isObject()) {
311 ✗ LogDebug(VB_COMMAND, "MQTT topic \"%s\" running command (JSON payload)\n", topic.c_str());
312 ✗ CommandManager::INSTANCE.run(val);
313 } else {
314 ✗ LogWarn(VB_COMMAND, "Invalid JSON Payload: %s\n", payload.c_str());
315 }
316 ✗ } else {
317 ✗ std::vector<std::string> args;
318 ✗ std::string command;
319
320 ✗ std::string ntopic = topic.substr(13); // remove /set/command/
321 ✗ args = splitWithQuotes(ntopic, '/');
322 ✗ command = args[0];
323 ✗ args.erase(args.begin());
324 ✗ bool foundp = false;
325 ✗ for (int x = 0; x < args.size(); x++) {
326 ✗ if (args[x] == "{Payload}") {
327 ✗ args[x] = payload;
328 ✗ foundp = true;
329 }
330 }
331 ✗ if (!payload.empty() && !foundp) {
332 ✗ args.push_back(payload);
333 }
334 ✗ if (args.size() == 0 && payload != "") {
335 ✗ Json::Value val = LoadJsonFromString(payload);
336 ✗ if (val.isObject()) {
337 ✗ LogDebug(VB_COMMAND, "MQTT topic \"%s\" running command \"%s\" (JSON payload)\n", topic.c_str(), command.c_str());
338 ✗ CommandManager::INSTANCE.run(command, val);
339 } else {
340 ✗ LogWarn(VB_COMMAND, "Invalid JSON Payload for topic %s: %s\n", topic.c_str(), payload.c_str());
341 }
342 ✗ } else {
343 ✗ LogDebug(VB_COMMAND, "MQTT topic \"%s\" running command \"%s\"\n", topic.c_str(), command.c_str());
344 ✗ CommandManager::INSTANCE.run(command, args);
345 }
346 ✗ }
347 1888 };
348
2/4
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
3776 Events::AddCallback("/set/command", f);
349
2/4
✓ Branch 0 taken 1888 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1888 times.
✗ Branch 3 not taken.
1888 Events::AddCallback("/set/command/#", f);
350 1888 }
351 458 CommandManager::~CommandManager() {
352 458 Cleanup();
353 458 }
354
355 4650 void CommandManager::Cleanup() {
356 // Idempotent: main() calls this during shutdown, and ~CommandManager() calls
357 // it again at static-destruction time. The FileMonitor::INSTANCE.RemoveFile()
358 // reach-in below locks a mutex on the global FileMonitor singleton, which may
359 // already be destroyed by then (cross-TU static destruction order) -- locking
360 // a destroyed mutex throws out of the noexcept dtor -> std::terminate. A
361 // function-local guard keeps the second call a no-op without changing the
362 // (plugin-facing) Commands.h ABI.
363 static std::atomic<bool> cleanedUp{false};
364
2/2
✓ Branch 0 taken 2325 times.
✓ Branch 1 taken 2325 times.
4650 if (cleanedUp.exchange(true)) {
365 2325 return;
366 }
367
5/10
✓ Branch 0 taken 2325 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2325 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2325 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2325 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2325 times.
✗ Branch 9 not taken.
6975 FileMonitor::INSTANCE.RemoveFile("CommandManager:CommandPresets.json", FPP_DIR_CONFIG("/commandPresets.json"));
368 2325 std::map<std::string, Command*> all;
369 {
370
1/2
✓ Branch 0 taken 2325 times.
✗ Branch 1 not taken.
2325 std::unique_lock<std::shared_mutex> l(commandsLock);
371 2325 all.swap(commands);
372 2325 commandMeta.clear();
373 2325 ++commandsGeneration;
374 2325 }
375
2/2
✓ Branch 0 taken 115795 times.
✓ Branch 1 taken 2325 times.
118120 for (auto& c : all) {
376 // An MQTT or HTTP thread can still be inside one at shutdown.
377
1/2
✓ Branch 0 taken 115795 times.
✗ Branch 1 not taken.
115795 waitForIdle(c.second);
378
1/2
✓ Branch 0 taken 115795 times.
✗ Branch 1 not taken.
115795 delete c.second;
379 }
380 2325 }
381
382 80 void CommandManager::addCommand(Command* cmd) {
383 // Plugin-facing entry point. Kept as a distinct, unchanged symbol (name and
384 // signature) so no plugin needs to be rebuilt. Anything registered through
385 // here is by definition not one of FPP's own categorized core commands, so
386 // it's tagged "Plugins" / level 0 (visible in Basic UI, per user decision:
387 // don't hide something a user deliberately installed).
388
2/4
✓ Branch 0 taken 80 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 80 times.
✗ Branch 3 not taken.
80 addCategorizedCommand(cmd, "Plugins", 0);
389 80 }
390 121075 void CommandManager::addCategorizedCommand(Command* cmd, const std::string& category, int level) {
391
1/2
✓ Branch 0 taken 121075 times.
✗ Branch 1 not taken.
121075 std::unique_lock<std::shared_mutex> l(commandsLock);
392
1/2
✓ Branch 0 taken 121075 times.
✗ Branch 1 not taken.
121075 auto a = commands.find(cmd->name);
393
5/6
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 121074 times.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 121074 times.
121075 if (a != commands.end() && a->second != cmd) {
394
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogWarn(VB_COMMAND, "Command \"%s\" registered twice; the later registration replaces the earlier\n", cmd->name.c_str());
395 }
396
1/2
✓ Branch 0 taken 121075 times.
✗ Branch 1 not taken.
121075 commands[cmd->name] = cmd;
397 121075 commandMeta[cmd->name] = { category, level };
398 121075 ++commandsGeneration;
399
2/6
✓ Branch 0 taken 121075 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 121075 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
242150 }
400
401 // Commands this thread is running, innermost last. waitForIdle() discounts
402 // them, so a command that unregisters itself from its own run() does not wait
403 // on itself.
404 static thread_local std::vector<Command*> tl_runningCommands;
405
406 194482 std::shared_ptr<Command> CommandManager::acquireCommand(const std::string& name) {
407
1/2
✓ Branch 0 taken 194482 times.
✗ Branch 1 not taken.
194482 std::shared_lock<std::shared_mutex> l(commandsLock);
408
1/2
✓ Branch 0 taken 194482 times.
✗ Branch 1 not taken.
194482 auto f = commands.find(name);
409
2/2
✓ Branch 0 taken 225 times.
✓ Branch 1 taken 194257 times.
194482 if (f == commands.end()) {
410 225 return nullptr;
411 }
412 194257 Command* cmd = f->second;
413 {
414
1/2
✓ Branch 0 taken 194257 times.
✗ Branch 1 not taken.
194257 std::lock_guard<std::mutex> il(inFlightLock);
415
1/2
✓ Branch 0 taken 194257 times.
✗ Branch 1 not taken.
194257 inFlight[cmd]++;
416 194257 }
417 // Releases the pin. Deletes only a command retireCommand() handed over
418 // while it ran; otherwise the command's owner does that.
419 194257 return std::shared_ptr<Command>(cmd, [this](Command* c) {
420 194257 Command* toDelete = nullptr;
421 {
422
1/2
✓ Branch 0 taken 194257 times.
✗ Branch 1 not taken.
194257 std::lock_guard<std::mutex> il(inFlightLock);
423
1/2
✓ Branch 0 taken 194257 times.
✗ Branch 1 not taken.
194257 auto it = inFlight.find(c);
424
5/6
✓ Branch 0 taken 194257 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 179348 times.
✓ Branch 3 taken 14909 times.
✓ Branch 4 taken 179348 times.
✓ Branch 5 taken 14909 times.
194257 if (it != inFlight.end() && --it->second <= 0) {
425
1/2
✓ Branch 0 taken 179348 times.
✗ Branch 1 not taken.
179348 inFlight.erase(it);
426
3/4
✓ Branch 0 taken 179348 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 179346 times.
179348 if (retired.erase(c)) {
427 2 toDelete = c;
428 }
429 }
430 194257 inFlightCV.notify_all();
431 194257 }
432
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 194255 times.
194257 delete toDelete;
433
1/2
✓ Branch 0 taken 194257 times.
✗ Branch 1 not taken.
388514 });
434 194482 }
435
436 // Every caller of removeCommand() deletes the command straight after, so it
437 // must not return while another thread is still inside that command's run().
438 // Bounded, because a run that never ends (one blocked on the thread doing the
439 // removal) would otherwise hang the removal for good; that case is logged.
440 119748 void CommandManager::waitForIdle(Command* cmd) {
441
1/2
✓ Branch 0 taken 119748 times.
✗ Branch 1 not taken.
119748 const int own = (int)std::count(tl_runningCommands.begin(), tl_runningCommands.end(), cmd);
442
1/2
✓ Branch 0 taken 119748 times.
✗ Branch 1 not taken.
119748 std::unique_lock<std::mutex> il(inFlightLock);
443
1/2
✓ Branch 0 taken 119748 times.
✗ Branch 1 not taken.
119748 bool idle = inFlightCV.wait_for(il, std::chrono::seconds(10), [&] {
444
1/2
✓ Branch 0 taken 119748 times.
✗ Branch 1 not taken.
119748 auto it = inFlight.find(cmd);
445
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 119748 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
119748 return it == inFlight.end() || it->second <= own;
446 });
447
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 119748 times.
119748 if (!idle) {
448 ✗ LogErr(VB_COMMAND, "Command \"%s\" is still running 10s after it was unregistered; continuing anyway\n", cmd->name.c_str());
449 }
450 119748 }
451
452 3952 void CommandManager::removeCommand(Command* cmd) {
453 {
454
1/2
✓ Branch 0 taken 3952 times.
✗ Branch 1 not taken.
3952 std::unique_lock<std::shared_mutex> l(commandsLock);
455
1/2
✓ Branch 0 taken 3952 times.
✗ Branch 1 not taken.
3952 auto a = commands.find(cmd->name);
456
6/6
✓ Branch 0 taken 3950 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 3949 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 3949 times.
✓ Branch 5 taken 3 times.
3952 if (a != commands.end() && a->second == cmd) {
457
1/2
✓ Branch 0 taken 3949 times.
✗ Branch 1 not taken.
3949 commands.erase(a);
458
1/2
✓ Branch 0 taken 3949 times.
✗ Branch 1 not taken.
3949 commandMeta.erase(cmd->name);
459 3949 ++commandsGeneration;
460 }
461 // else another object holds this name now (two outputs registering
462 // the same command, or a reload's replacement registering first);
463 // removing by name would unregister theirs.
464 3952 }
465 // Waited for either way: a replaced command can still be running from
466 // before its replacement, and the caller deletes it next.
467 3952 waitForIdle(cmd);
468 3952 }
469 2 void CommandManager::retireCommand(Command* cmd) {
470 {
471
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 std::unique_lock<std::shared_mutex> l(commandsLock);
472
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 auto a = commands.find(cmd->name);
473
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 if (a != commands.end() && a->second == cmd) {
474
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 commands.erase(a);
475
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 commandMeta.erase(cmd->name);
476 2 ++commandsGeneration;
477 }
478 2 }
479 // Unregistered first, so no new run can pin it: any run still counted
480 // here started before, and the last one to return deletes it. Nothing
481 // waits, so a run that outlasts any bound is not freed under itself.
482 {
483
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 std::lock_guard<std::mutex> il(inFlightLock);
484
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 if (inFlight.find(cmd) != inFlight.end()) {
485
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 retired.insert(cmd);
486 2 return;
487 }
488
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 }
489 ✗ delete cmd;
490 }
491 1 void CommandManager::removeCommand(const std::string& cmdName) {
492 1 Command* cmd = nullptr;
493 {
494
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 std::unique_lock<std::shared_mutex> l(commandsLock);
495
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 auto a = commands.find(cmdName);
496
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (a != commands.end()) {
497 1 cmd = a->second;
498
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 commands.erase(a);
499 }
500
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 commandMeta.erase(cmdName);
501 1 ++commandsGeneration;
502 1 }
503
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (cmd) {
504 1 waitForIdle(cmd);
505 }
506 1 }
507 297 std::vector<std::pair<std::string, Command*>> CommandManager::getRegisteredCommands() const {
508
1/2
✓ Branch 0 taken 297 times.
✗ Branch 1 not taken.
297 std::shared_lock<std::shared_mutex> l(commandsLock);
509 297 std::vector<std::pair<std::string, Command*>> ret;
510
1/2
✓ Branch 0 taken 297 times.
✗ Branch 1 not taken.
297 ret.reserve(commands.size());
511
2/2
✓ Branch 0 taken 18023 times.
✓ Branch 1 taken 297 times.
18320 for (const auto& c : commands) {
512
1/2
✓ Branch 0 taken 18023 times.
✗ Branch 1 not taken.
18023 ret.emplace_back(c.first, c.second);
513 }
514 297 return ret;
515 297 }
516 // Caller holds commandsLock (shared).
517 3837 Json::Value CommandManager::describeCommand(Command* cmd) {
518
2/2
✓ Branch 0 taken 3836 times.
✓ Branch 1 taken 1 times.
3837 Json::Value result = cmd->getDescription();
519
1/2
✓ Branch 0 taken 3836 times.
✗ Branch 1 not taken.
3836 auto m = commandMeta.find(cmd->name);
520
1/2
✓ Branch 0 taken 3836 times.
✗ Branch 1 not taken.
3836 if (m != commandMeta.end()) {
521
2/4
✓ Branch 0 taken 3836 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3836 times.
✗ Branch 3 not taken.
3836 result["category"] = m->second.category;
522
2/4
✓ Branch 0 taken 3836 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3836 times.
✗ Branch 3 not taken.
3836 result["level"] = m->second.level;
523 }
524 3836 return result;
525 ✗ }
526 60 Json::Value CommandManager::getDescriptions() {
527
1/2
✓ Branch 0 taken 60 times.
✗ Branch 1 not taken.
60 Json::Value ret;
528
1/2
✓ Branch 0 taken 60 times.
✗ Branch 1 not taken.
60 std::shared_lock<std::shared_mutex> l(commandsLock);
529
2/2
✓ Branch 0 taken 3897 times.
✓ Branch 1 taken 60 times.
3957 for (auto& a : commands) {
530 // Isolate each command's description: a single command that throws while
531 // being described (e.g. a stale external plugin whose Command/CommandArg
532 // layout no longer matches these headers, so getDescription() reads a
533 // garbage std::string and jsoncpp rejects its length) must not take out
534 // the whole /commands endpoint - and the UI that depends on it - for
535 // every other command. Log and skip the offender instead. The map key
536 // (a.first) is FPP's own string, safe to read even when the command
537 // object behind it is not. (This only catches C++ exceptions; a hard
538 // memory fault in a plugin is a separate failure the vtable/ABI rules
539 // in Commands.h exist to prevent.)
540 try {
541
3/4
✓ Branch 0 taken 3897 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3837 times.
✓ Branch 3 taken 60 times.
3897 if (!a.second->hidden()) {
542
3/4
✓ Branch 0 taken 3836 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 3836 times.
✗ Branch 3 not taken.
3837 ret.append(describeCommand(a.second));
543 }
544
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 } catch (const std::exception& e) {
545
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogWarn(VB_COMMAND, "Skipping command \"%s\" in /commands: %s\n",
546 a.first.c_str(), e.what());
547 1 } catch (...) {
548 ✗ LogWarn(VB_COMMAND, "Skipping command \"%s\" in /commands: unknown exception\n",
549 a.first.c_str());
550 ✗ }
551 }
552 60 return ret;
553 60 }
554
555 // Flattens a command's args into the "a,b,c" form used by the log lines below,
556 // so every path a command can arrive on reports it the same way: as
557 // "command(args)". Args are unbounded (one could carry a Variable's full
558 // value), so each is truncated before logging - see TruncateForLog().
559 //
560 // Deliberately NOT gated behind WillLog() at the call sites. Every caller logs
561 // at Info, and _LogWrite() formats any line at Debug or below regardless of the
562 // configured level so the crash ring can retain it (see CrashLogRingWillCapture)
563 // - so a guard would not skip the formatting, it would only drop the line out of
564 // crash dumps. Guarding is a pure win only at LOG_EXCESSIVE, which the ring
565 // never captures; that is the one level FPP's other WillLog() call sites use.
566 194261 static std::string FormatArgsForLog(const std::vector<std::string>& args) {
567 194261 std::string argString;
568
2/2
✓ Branch 0 taken 436905 times.
✓ Branch 1 taken 194261 times.
631166 for (auto& a : args) {
569
2/2
✓ Branch 0 taken 277752 times.
✓ Branch 1 taken 159153 times.
436905 if (!argString.empty()) {
570
1/2
✓ Branch 0 taken 277752 times.
✗ Branch 1 not taken.
277752 argString += ",";
571 }
572
2/4
✓ Branch 0 taken 436905 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 436905 times.
✗ Branch 3 not taken.
436905 argString += TruncateForLog(a);
573 }
574 194261 return argString;
575 ✗ }
576
577 // The one place a Command is actually invoked, so the one place its argument
578 // contract can be enforced.
579 //
580 // A command that declares arguments reads args[0] to get its subject - the
581 // playlist name, the media file, the volume. Most check that it is there
582 // first; several index it bare, and operator[](0) on an empty vector hands
583 // run() a reference to nothing. That is not a theoretical hazard: a bare
584 // "/api/command/Play Media" (no arguments at all) walked a garbage
585 // std::string down to GStreamerOutput's constructor and segfaulted there,
586 // and "Insert Playlist Immediate" did the same through Playlist::Play - the
587 // POST form reaching zero args because a form-urlencoded body fails to parse
588 // as JSON and leaves an empty array behind.
589 //
590 // Enforcing it here rather than in each run() covers every entry path at once
591 // - GET, POST, MQTT, presets, GPIO, playlist entries, MultiSync - and covers
592 // plugin commands, which FPP cannot audit. A command that genuinely works
593 // with no arguments says so by declaring its first argument optional (or by
594 // declaring none).
595 194256 static std::unique_ptr<Command::Result> invokeCommand(Command* cmd, const std::vector<std::string>& args) {
596
8/8
✓ Branch 0 taken 35120 times.
✓ Branch 1 taken 159136 times.
✓ Branch 2 taken 45 times.
✓ Branch 3 taken 35075 times.
✓ Branch 4 taken 42 times.
✓ Branch 5 taken 3 times.
✓ Branch 6 taken 42 times.
✓ Branch 7 taken 194214 times.
194256 if (args.empty() && !cmd->args.empty() && !cmd->args.front().optional) {
597 42 const std::string& argName = cmd->args.front().name;
598
1/2
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
42 LogWarn(VB_COMMAND, "Command \"%s\" requires the \"%s\" argument, none given - not running it\n",
599 cmd->name.c_str(), argName.c_str());
600
1/2
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
84 return std::make_unique<Command::ErrorResult>(
601
4/8
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 42 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 42 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 42 times.
✗ Branch 7 not taken.
126 "Command \"" + cmd->name + "\" requires the \"" + argName + "\" argument");
602 }
603 // ...and the one place every nested command passes through, so the one
604 // place runaway nesting can be bounded. Commands run commands: a preset, an
605 // If's Then/Else list, a playlist entry that restarts its own playlist, a
606 // %VAR: whose value is an If that expands the same variable again. Each
607 // path that recurses into itself on the same stack has its own fix, but
608 // anything missed - including a plugin command - would otherwise grow the
609 // stack until it faults. Real chains are a handful deep.
610 static constexpr int MAX_COMMAND_DEPTH = 32;
611 static thread_local int depth = 0;
612
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 194214 times.
194214 if (depth >= MAX_COMMAND_DEPTH) {
613 ✗ LogErr(VB_COMMAND, "Command \"%s\" not run: commands nested more than %d deep - a command is running itself\n",
614 cmd->name.c_str(), MAX_COMMAND_DEPTH);
615 ✗ return std::make_unique<Command::ErrorResult>("Command \"" + cmd->name + "\" nested too deep");
616 }
617 struct DepthGuard {
618 Command* c;
619 194214 explicit DepthGuard(Command* cmd) :
620 194214 c(cmd) {
621 194214 depth++;
622 194214 tl_runningCommands.push_back(c);
623 194214 }
624 194214 ~DepthGuard() {
625 194214 depth--;
626 194214 tl_runningCommands.pop_back();
627 194214 }
628
1/2
✓ Branch 0 taken 194214 times.
✗ Branch 1 not taken.
194214 } depthGuard(cmd);
629
630 // ...and the one place a throwing command can be stopped from taking fppd
631 // down. Only the HTTP routes have a catch above them (drogon's); MQTT runs
632 // commands on libmosquitto's thread, MultiSync, GPIO, presets and playlist
633 // entries on the main loop, and none of those frames catch, so an exception
634 // out of run() - jsoncpp's LogicError on an argument of the wrong JSON type,
635 // std::stoi on a non-number, a Magick error, any plugin's own throw - was
636 // std::terminate. A malformed command is the sender's error; report it as
637 // one.
638 try {
639
2/2
✓ Branch 0 taken 194209 times.
✓ Branch 1 taken 5 times.
194214 return cmd->run(args);
640
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 } catch (const std::exception& e) {
641
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 LogErr(VB_COMMAND, "Command \"%s(%s)\" failed: %s\n", cmd->name.c_str(), FormatArgsForLog(args).c_str(), e.what());
642
4/8
✓ 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.
✓ Branch 6 taken 5 times.
✗ Branch 7 not taken.
5 return std::make_unique<Command::ErrorResult>("Command \"" + cmd->name + "\" failed: " + e.what());
643 5 } catch (...) {
644 ✗ LogErr(VB_COMMAND, "Command \"%s(%s)\" failed with an unknown exception\n", cmd->name.c_str(), FormatArgsForLog(args).c_str());
645 ✗ return std::make_unique<Command::ErrorResult>("Command \"" + cmd->name + "\" failed");
646 ✗ }
647 194214 }
648
649 // A command's args as strings, whatever JSON type each one arrived as. Every
650 // documented sender writes strings, but these come off MQTT payloads, MultiSync
651 // packets and hand-edited presets, and asString() throws on an object or array
652 // - and indexing a non-array by position throws too. Scalars convert the way
653 // asString() always has; an object or array is passed on as its JSON text,
654 // which is what the commands that take structured args (If) parse anyway.
655 193364 static bool JsonArgsToStrings(const Json::Value& argsArray, std::vector<std::string>& args, std::string& err) {
656
2/2
✓ Branch 0 taken 51 times.
✓ Branch 1 taken 193313 times.
193364 if (argsArray.isNull()) {
657 51 return true;
658 }
659
2/2
✓ Branch 0 taken 103 times.
✓ Branch 1 taken 193210 times.
193313 if (!argsArray.isArray()) {
660 103 err = "\"args\" must be an array";
661 103 return false;
662 }
663
2/2
✓ Branch 0 taken 429916 times.
✓ Branch 1 taken 193210 times.
623126 for (Json::ArrayIndex x = 0; x < argsArray.size(); x++) {
664 429916 const Json::Value& a = argsArray[x];
665
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 429916 times.
429916 if (a.isNull()) {
666 ✗ args.push_back("");
667
5/6
✓ Branch 0 taken 429864 times.
✓ Branch 1 taken 52 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 429864 times.
✓ Branch 4 taken 52 times.
✓ Branch 5 taken 429864 times.
429916 } else if (a.isObject() || a.isArray()) {
668
3/6
✓ 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.
104 args.push_back(SaveJsonToString(a));
669 } else {
670
2/4
✓ Branch 0 taken 429864 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 429864 times.
✗ Branch 3 not taken.
429864 args.push_back(a.asString());
671 }
672 }
673 193210 return true;
674 }
675
676 // A boolean flag in a command's JSON, tolerating the "true"/"1" string forms a
677 // hand-written payload uses; asBool() throws on a string.
678 17 static bool JsonFlag(const Json::Value& v) {
679
1/2
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
17 if (v.isBool()) {
680 17 return v.asBool();
681 }
682 ✗ if (v.isIntegral()) {
683 ✗ return v.asLargestInt() != 0;
684 }
685 ✗ if (v.isString()) {
686 ✗ std::string s = v.asString();
687 ✗ return s == "true" || s == "1";
688 ✗ }
689 ✗ return false;
690 }
691
692 1052 std::unique_ptr<Command::Result> CommandManager::run(const std::string& command, const std::vector<std::string>& args) {
693 // Pinned: removeCommand() waits for it, so it cannot be freed mid-run.
694
1/2
✓ Branch 0 taken 1052 times.
✗ Branch 1 not taken.
1052 std::shared_ptr<Command> pinned = acquireCommand(command);
695
2/2
✓ Branch 0 taken 1046 times.
✓ Branch 1 taken 6 times.
1052 if (pinned) {
696
1/2
✓ Branch 0 taken 1046 times.
✗ Branch 1 not taken.
1046 std::vector<std::string> resolvedArgs = args;
697
1/2
✓ Branch 0 taken 1046 times.
✗ Branch 1 not taken.
1046 ResolveCommandArgs(pinned.get(), resolvedArgs);
698
699 // Logged after substitution (like the Json overload below) so the line
700 // shows what actually ran, not the unresolved %VAR:name% that was
701 // passed in.
702
2/4
✓ Branch 0 taken 1046 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1046 times.
✗ Branch 3 not taken.
1046 LogInfo(VB_COMMAND, "Running command \"%s(%s)\"\n", command.c_str(), FormatArgsForLog(resolvedArgs).c_str());
703
704 // Publish MQTT event for command execution
705
1/2
✓ Branch 0 taken 1046 times.
✗ Branch 1 not taken.
1046 Json::Value payload;
706
2/4
✓ Branch 0 taken 1046 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1046 times.
✗ Branch 3 not taken.
1046 payload["command"] = command;
707
2/4
✓ Branch 0 taken 1046 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1046 times.
✗ Branch 3 not taken.
1046 payload["args"] = Json::Value(Json::arrayValue);
708
2/2
✓ Branch 0 taken 7334 times.
✓ Branch 1 taken 1046 times.
8380 for (const auto& arg : resolvedArgs) {
709
3/6
✓ Branch 0 taken 7334 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7334 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 7334 times.
✗ Branch 5 not taken.
7334 payload["args"].append(arg);
710 }
711
2/4
✓ Branch 0 taken 1046 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1046 times.
✗ Branch 3 not taken.
1046 payload["trigger"] = "internal";
712
1/2
✓ Branch 0 taken 1046 times.
✗ Branch 1 not taken.
2092 std::string topic = "command/run";
713
3/6
✓ Branch 0 taken 1046 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1046 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1046 times.
✗ Branch 5 not taken.
1046 Events::Publish(topic, SaveJsonToString(payload));
714
715
1/2
✓ Branch 0 taken 1046 times.
✗ Branch 1 not taken.
1046 return invokeCommand(pinned.get(), resolvedArgs);
716 1046 }
717
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 LogWarn(VB_COMMAND, "No command found for \"%s\"\n", command.c_str());
718 // A playlist "FPP Command" entry (or preset, GPIO binding, etc.) can
719 // reference a command that a plugin used to provide but no longer does
720 // (uninstalled/updated) - the failure above is otherwise silent: the
721 // caller just gets an ErrorResult and, for a playlist entry, that item is
722 // marked finished exactly like a successful one (PlaylistEntryCommand::
723 // StartPlaying() -> FinishPlay(), no distinct failure state), so a show
724 // can silently skip this step forever with nothing but a log line to
725 // show for it. Surface it as a real warning instead.
726
4/8
✓ 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.
✓ Branch 6 taken 6 times.
✗ Branch 7 not taken.
6 WarningHolder::AddWarningTimeout(900, 61, "No command found for \"" + command + "\" - it may have come from a plugin that is no longer installed or was updated");
727
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
6 return std::make_unique<Command::ErrorResult>("No Command: " + command);
728 1052 }
729
730 193216 std::unique_ptr<Command::Result> CommandManager::run(const std::string& command, const Json::Value& argsArray) {
731 // Pinned: removeCommand() waits for it, so it cannot be freed mid-run.
732
1/2
✓ Branch 0 taken 193216 times.
✗ Branch 1 not taken.
193216 std::shared_ptr<Command> pinned = acquireCommand(command);
733
2/2
✓ Branch 0 taken 193167 times.
✓ Branch 1 taken 49 times.
193216 if (pinned) {
734 193167 std::vector<std::string> args;
735 193167 std::string err;
736
3/4
✓ Branch 0 taken 193167 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 193166 times.
193167 if (!JsonArgsToStrings(argsArray, args, err)) {
737
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogWarn(VB_COMMAND, "Command \"%s\" not run: %s\n", command.c_str(), err.c_str());
738
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 return std::make_unique<Command::ErrorResult>(err);
739 }
740
1/2
✓ Branch 0 taken 193166 times.
✗ Branch 1 not taken.
193166 ResolveCommandArgs(pinned.get(), args);
741
2/4
✓ Branch 0 taken 193166 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 193166 times.
✗ Branch 3 not taken.
193166 LogInfo(VB_COMMAND, "Running command \"%s(%s)\"\n", command.c_str(), FormatArgsForLog(args).c_str());
742
743 // Publish MQTT event for command execution
744
1/2
✓ Branch 0 taken 193166 times.
✗ Branch 1 not taken.
193166 Json::Value payload;
745
2/4
✓ Branch 0 taken 193166 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 193166 times.
✗ Branch 3 not taken.
193166 payload["command"] = command;
746
2/4
✓ Branch 0 taken 193166 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 193166 times.
✗ Branch 3 not taken.
193166 payload["args"] = Json::Value(Json::arrayValue);
747
2/2
✓ Branch 0 taken 429514 times.
✓ Branch 1 taken 193166 times.
622680 for (const auto& arg : args) {
748
3/6
✓ Branch 0 taken 429514 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 429514 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 429514 times.
✗ Branch 5 not taken.
429514 payload["args"].append(arg);
749 }
750
2/4
✓ Branch 0 taken 193166 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 193166 times.
✗ Branch 3 not taken.
193166 payload["trigger"] = "ui";
751
1/2
✓ Branch 0 taken 193166 times.
✗ Branch 1 not taken.
386332 std::string topic = "command/run";
752
2/4
✓ Branch 0 taken 193166 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 193166 times.
✗ Branch 3 not taken.
193166 std::string payloadStr = SaveJsonToString(payload);
753
2/4
✓ Branch 0 taken 193166 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 193166 times.
✗ Branch 3 not taken.
193166 LogExcess(VB_COMMAND, "JSONVAL MQTT Publishing command: %s, payload: %s\n", topic.c_str(), TruncateForLog(payloadStr, 2000).c_str());
754
1/2
✓ Branch 0 taken 193166 times.
✗ Branch 1 not taken.
193166 Events::Publish(topic, payloadStr);
755
756
1/2
✓ Branch 0 taken 193166 times.
✗ Branch 1 not taken.
193166 return invokeCommand(pinned.get(), args);
757 193167 }
758
1/2
✓ Branch 0 taken 49 times.
✗ Branch 1 not taken.
49 LogWarn(VB_COMMAND, "No command found for \"%s\"\n", command.c_str());
759 // A playlist "FPP Command" entry (or preset, GPIO binding, etc.) can
760 // reference a command that a plugin used to provide but no longer does
761 // (uninstalled/updated) - the failure above is otherwise silent: the
762 // caller just gets an ErrorResult and, for a playlist entry, that item is
763 // marked finished exactly like a successful one (PlaylistEntryCommand::
764 // StartPlaying() -> FinishPlay(), no distinct failure state), so a show
765 // can silently skip this step forever with nothing but a log line to
766 // show for it. Surface it as a real warning instead.
767
4/8
✓ Branch 0 taken 49 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 49 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 49 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 49 times.
✗ Branch 7 not taken.
49 WarningHolder::AddWarningTimeout(900, 61, "No command found for \"" + command + "\" - it may have come from a plugin that is no longer installed or was updated");
768
2/4
✓ Branch 0 taken 49 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 49 times.
✗ Branch 3 not taken.
49 return std::make_unique<Command::ErrorResult>("No Command: " + command);
769 193216 }
770 193201 std::unique_ptr<Command::Result> CommandManager::run(const Json::Value& cmd) {
771
6/12
✓ Branch 0 taken 193201 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 193201 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 193201 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 193201 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 193201 times.
✗ Branch 10 not taken.
✓ Branch 11 taken 193201 times.
193201 if (!cmd.isObject() || !cmd["command"].isString()) {
772 ✗ LogWarn(VB_COMMAND, "Command not run: \"command\" must be a string\n");
773 ✗ return std::make_unique<Command::ErrorResult>("\"command\" must be a string");
774 }
775
2/4
✓ Branch 0 taken 193201 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 193201 times.
✗ Branch 3 not taken.
193201 std::string command = cmd["command"].asString();
776
3/4
✓ Branch 0 taken 193201 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✓ Branch 3 taken 193184 times.
193201 if (cmd.isMember("multisyncCommand")) {
777
2/4
✓ Branch 0 taken 17 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 17 times.
✗ Branch 3 not taken.
17 bool multisync = JsonFlag(cmd["multisyncCommand"]);
778
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 9 times.
17 if (multisync) {
779
6/16
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 8 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 8 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
✓ Branch 13 taken 8 times.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
8 std::string hosts = cmd["multisyncHosts"].isString() ? cmd["multisyncHosts"].asString() : "";
780 8 std::vector<std::string> args;
781 8 std::string err;
782
3/6
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 8 times.
8 if (!JsonArgsToStrings(cmd["args"], args, err)) {
783 ✗ LogWarn(VB_COMMAND, "Command \"%s\" not sent: %s\n", command.c_str(), err.c_str());
784 ✗ return std::make_unique<Command::ErrorResult>(err);
785 }
786 // Resolved against THIS host's Variables before sending - a remote
787 // host has no way to resolve %VAR:name% itself (and may not even
788 // have a same-named Variable), so it must go out already
789 // substituted, unlike the non-multisync path below where run()
790 // does this.
791
2/2
✓ Branch 0 taken 337 times.
✓ Branch 1 taken 8 times.
345 for (auto& a : args) {
792
2/4
✓ Branch 0 taken 337 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 337 times.
✗ Branch 3 not taken.
337 a = ReplaceVariableKeywords(a);
793 }
794
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 MultiSync::INSTANCE.SendFPPCommandPacket(hosts, command, args);
795
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 return std::make_unique<Command::Result>("Command Sent");
796 8 }
797 }
798
2/4
✓ Branch 0 taken 193193 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 193193 times.
✗ Branch 3 not taken.
193193 return run(command, cmd["args"]);
799 193201 }
800
801 // --------------------------------------------------------------------------
802 // OpenAPI docs for the /command(s) and /commandPresets endpoints handled below.
803 // --------------------------------------------------------------------------
804
805 /**
806 * List all available commands and their argument descriptions. Each entry
807 * also carries "category" (e.g. "Playlist", "Media", "Plugins") and "level"
808 * (0 Basic / 1 Advanced / 3 Developer) for UI grouping and filtering, plus
809 * "disallowMultisync" (true) on a command whose Multisync option should stay
810 * hidden - e.g. "If", since multisyncing it would broadcast the raw check to
811 * other instances rather than propagate the result of evaluating it.
812 *
813 * @route GET /api/commands
814 * @response 200 Array of command descriptions.
815 */
816
817 /**
818 * Get the description of a single command by name. Includes the same
819 * "category"/"level" fields as the list route.
820 *
821 * @route GET /api/commands/{command}
822 * @response 200 The command description.
823 * @response 404 No command with that name exists.
824 */
825
826 /**
827 * Get the saved command presets (config/commandPresets.json).
828 *
829 * @route GET /api/commandPresets
830 * @param boolean names Return just the preset names instead of full definitions.
831 * @response 200 Command presets (or preset names when `names=true`).
832 */
833
834 /**
835 * Get a single command preset by name.
836 *
837 * @route GET /api/commandPresets/{name}
838 * @response 200 The preset definition.
839 */
840
841 /**
842 * Run a command by name via GET, passing arguments as extra path segments
843 * (e.g. /api/command/Volume%20Set/50).
844 *
845 * An argument may contain `%VAR:name%` to substitute a User Variable's current
846 * value, as anywhere else a command's arguments are given. Percent-encode it as
847 * `%25VAR:name%25`: sent raw, `%VA` is an invalid escape and Apache rejects the
848 * request with a 400 before it reaches FPP. An unset variable substitutes as an
849 * empty string.
850 *
851 * Note that empty path segments collapse, so an argument that should be empty
852 * cannot be passed this way - use the POST form below for that.
853 *
854 * Deprecated: FPP 11 will require one of the POST forms below.
855 *
856 * @route GET /api/command/{command} deprecated
857 * @response 200 Command result (text/plain).
858 * @response 404 No command with that name exists.
859 * @response 500 The command errored or timed out.
860 */
861 // GET /api/command/<name>/... runs a command, so over plain HTTP any web page
862 // can make a browser run one. FPP 11 will require POST (docs/DEPRECATED.md);
863 // until then it works, carries a Deprecation header, and is logged at most
864 // once an hour per command and client so its callers can be found.
865 42 static void NoteDeprecatedCommandGet(const std::string& command, const std::string& client) {
866 static std::mutex lock;
867
3/4
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 37 times.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
42 static std::map<std::string, uint64_t> lastLogged;
868
1/2
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
42 uint64_t now = GetTimeMS();
869
2/4
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 42 times.
✗ Branch 3 not taken.
42 std::string key = command + "|" + client;
870
1/2
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
42 std::unique_lock<std::mutex> l(lock);
871
1/2
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
42 auto it = lastLogged.find(key);
872
5/6
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 40 times.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 40 times.
42 if (it != lastLogged.end() && now - it->second < 3600 * 1000ULL) {
873 2 return;
874 }
875
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 40 times.
40 if (lastLogged.size() > 256) {
876 ✗ lastLogged.clear();
877 }
878
1/2
✓ Branch 0 taken 40 times.
✗ Branch 1 not taken.
40 lastLogged[key] = now;
879
1/2
✓ Branch 0 taken 40 times.
✗ Branch 1 not taken.
40 l.unlock();
880
1/2
✓ Branch 0 taken 40 times.
✗ Branch 1 not taken.
40 LogWarn(VB_COMMAND, "Deprecated: GET /api/command/%s from %s; FPP 11 will require POST /api/command (docs/DEPRECATED.md)\n",
881 command.c_str(), client.c_str());
882
4/4
✓ Branch 0 taken 40 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 40 times.
✓ Branch 3 taken 2 times.
44 }
883
884 375 HttpResponsePtr CommandManager::render_GET(const HttpRequestPtr& req) {
885
2/4
✓ Branch 0 taken 375 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 375 times.
✗ Branch 3 not taken.
375 auto parts = getPathPieces(req->path());
886 375 int plen = parts.size();
887
1/2
✓ Branch 0 taken 375 times.
✗ Branch 1 not taken.
375 std::string p1 = parts[0];
888
889
3/4
✓ Branch 0 taken 375 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 146 times.
✓ Branch 3 taken 229 times.
375 if (p1 == "commands") {
890
2/2
✓ Branch 0 taken 85 times.
✓ Branch 1 taken 61 times.
146 if (plen > 1) {
891
1/2
✓ Branch 0 taken 85 times.
✗ Branch 1 not taken.
85 std::string command = parts[1];
892
1/2
✓ Branch 0 taken 85 times.
✗ Branch 1 not taken.
85 std::shared_lock<std::shared_mutex> cl(commandsLock);
893
1/2
✓ Branch 0 taken 85 times.
✗ Branch 1 not taken.
85 auto f = commands.find(command);
894
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 85 times.
85 if (f != commands.end()) {
895 ✗ Json::Value result = describeCommand(f->second);
896 ✗ std::string resultStr = SaveJsonToString(result, " ");
897 ✗ return makeStringResponse(resultStr, 200, "application/json");
898 ✗ }
899
2/4
✓ Branch 0 taken 85 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 85 times.
✗ Branch 3 not taken.
85 } else {
900 // The full list is ~33KB and every page that can edit a command
901 // fetches it twice on load (PopulateCommandListCache and
902 // captureCommandListJSON), plus once more after an fppd restart.
903 // Its content is fixed until something registers or unregisters a
904 // command, so the generation counter answers a conditional request
905 // without describing all of them again.
906
2/4
✓ Branch 0 taken 61 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 61 times.
✗ Branch 3 not taken.
244 return makeVersionedETagResponse(req, std::to_string(commandsGeneration.load()), [this]() {
907
1/2
✓ Branch 0 taken 60 times.
✗ Branch 1 not taken.
60 Json::Value result = getDescriptions();
908
2/4
✓ Branch 0 taken 60 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 60 times.
✗ Branch 3 not taken.
120 return SaveJsonToString(result, " ");
909
1/2
✓ Branch 0 taken 61 times.
✗ Branch 1 not taken.
121 });
910 }
911
3/4
✓ Branch 0 taken 229 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 97 times.
✓ Branch 3 taken 132 times.
229 } else if (p1 == "commandPresets") {
912
3/6
✓ Branch 0 taken 97 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 97 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 97 times.
✗ Branch 5 not taken.
97 std::string commandsFile = FPP_DIR_CONFIG("/commandPresets.json");
913
1/2
✓ Branch 0 taken 97 times.
✗ Branch 1 not taken.
97 Json::Value allCommands;
914
2/4
✓ Branch 0 taken 97 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 97 times.
✗ Branch 3 not taken.
97 if (FileExists(commandsFile)) {
915 // Load new config file
916
1/2
✓ Branch 0 taken 97 times.
✗ Branch 1 not taken.
97 allCommands = LoadJsonFromFile(commandsFile, JsonRoot::Object);
917 }
918
2/2
✓ Branch 0 taken 85 times.
✓ Branch 1 taken 12 times.
97 if (plen > 1) {
919
2/4
✓ Branch 0 taken 85 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 85 times.
✗ Branch 3 not taken.
85 if (allCommands.isMember("commands")) {
920
1/2
✓ Branch 0 taken 85 times.
✗ Branch 1 not taken.
85 std::string p2 = parts[1];
921
4/6
✓ Branch 0 taken 170 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 170 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 85 times.
✓ Branch 5 taken 85 times.
170 for (int x = 0; x < allCommands["commands"].size(); x++) {
922
5/10
✓ Branch 0 taken 85 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 85 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 85 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 85 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 85 times.
85 if (allCommands["commands"][x]["name"].asString() == p2) {
923 ✗ std::string resultStr = SaveJsonToString(allCommands["commands"][x], " ");
924 ✗ return makeStringResponse(resultStr, 200, "application/json");
925 ✗ }
926 }
927
1/2
✓ Branch 0 taken 85 times.
✗ Branch 1 not taken.
85 }
928 } else {
929
5/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 2 times.
✓ Branch 7 taken 10 times.
24 if (getRequestArg(req, "names") == "true") {
930
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 Json::Value names;
931
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 if (allCommands.isMember("commands")) {
932
4/6
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 2 times.
4 for (int x = 0; x < allCommands["commands"].size(); x++) {
933
6/12
✓ 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.
✓ Branch 8 taken 2 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 2 times.
✗ Branch 11 not taken.
2 names.append(allCommands["commands"][x]["name"].asString());
934 }
935 }
936
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
4 std::string resultStr = SaveJsonToString(names, " ");
937
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 return makeStringResponse(resultStr, 200, "application/json");
938 2 }
939
2/4
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
✗ Branch 3 not taken.
20 std::string resultStr = SaveJsonToString(allCommands, " ");
940
2/4
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
✗ Branch 3 not taken.
10 return makeStringResponse(resultStr, 200, "application/json");
941 10 }
942
10/12
✓ Branch 0 taken 85 times.
✓ Branch 1 taken 12 times.
✓ Branch 2 taken 85 times.
✓ Branch 3 taken 12 times.
✓ Branch 4 taken 132 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 132 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 127 times.
✓ Branch 9 taken 5 times.
✓ Branch 10 taken 127 times.
✓ Branch 11 taken 5 times.
241 } else if (p1 == "command" && plen > 1) {
943
1/2
✓ Branch 0 taken 127 times.
✗ Branch 1 not taken.
127 std::string command = parts[1];
944 127 std::vector<std::string> args;
945
2/2
✓ Branch 0 taken 46 times.
✓ Branch 1 taken 127 times.
173 for (int x = 2; x < plen; x++) {
946
1/2
✓ Branch 0 taken 46 times.
✗ Branch 1 not taken.
46 args.push_back(parts[x]);
947 }
948 // Pinned: removeCommand() waits for it, so it cannot be freed mid-run.
949
1/2
✓ Branch 0 taken 127 times.
✗ Branch 1 not taken.
127 std::shared_ptr<Command> pinned = acquireCommand(command);
950
2/2
✓ Branch 0 taken 42 times.
✓ Branch 1 taken 85 times.
127 if (pinned) {
951 // Substituted here rather than inherited from CommandManager::run():
952 // this route calls the Command directly (see the MQTT publish below
953 // for why), so without this %VAR:name% would pass through literally
954 // on /api/command/... alone while resolving on every other path.
955
1/2
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
42 ResolveCommandArgs(pinned.get(), args);
956 // This route calls the Command directly rather than through
957 // CommandManager::run(), so it must log the args itself - see the
958 // MQTT publish just below for why it can't simply delegate.
959
3/6
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 42 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 42 times.
✗ Branch 5 not taken.
42 LogInfo(VB_COMMAND, "GET /api/command/%s from %s: running command \"%s(%s)\"\n",
960 command.c_str(), getEffectiveClientIP(req).c_str(),
961 command.c_str(), FormatArgsForLog(args).c_str());
962
963 // Publish MQTT event for command execution
964
1/2
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
42 Json::Value payload;
965
2/4
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 42 times.
✗ Branch 3 not taken.
42 payload["command"] = command;
966
2/4
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 42 times.
✗ Branch 3 not taken.
42 payload["args"] = Json::Value(Json::arrayValue);
967
2/2
✓ Branch 0 taken 21 times.
✓ Branch 1 taken 42 times.
63 for (const auto& arg : args) {
968
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 payload["args"].append(arg);
969 }
970
2/4
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 42 times.
✗ Branch 3 not taken.
42 payload["trigger"] = "api-get";
971
1/2
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
84 std::string topic = "command/run";
972
2/4
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 42 times.
✗ Branch 3 not taken.
42 std::string payloadStr = SaveJsonToString(payload);
973
2/4
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 42 times.
✗ Branch 3 not taken.
42 LogExcess(VB_COMMAND, "GET MQTT Publishing command: %s, payload: %s\n", topic.c_str(), TruncateForLog(payloadStr, 2000).c_str());
974
1/2
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
42 Events::Publish(topic, payloadStr);
975
976
2/4
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 42 times.
✗ Branch 3 not taken.
42 NoteDeprecatedCommandGet(command, getEffectiveClientIP(req));
977 42 auto deprecated = [](HttpResponsePtr resp) {
978
3/6
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 42 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 42 times.
✗ Branch 5 not taken.
168 resp->addHeader("Deprecation", "true");
979 42 return resp;
980 };
981
982
1/2
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
42 std::unique_ptr<Command::Result> r = invokeCommand(pinned.get(), args);
983 42 int count = 0;
984
3/8
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 42 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 42 times.
42 while (!r->isDone() && count < 1000) {
985 ✗ std::this_thread::sleep_for(std::chrono::milliseconds(1));
986 ✗ count++;
987 }
988
2/4
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 42 times.
✗ Branch 3 not taken.
42 if (r->isDone()) {
989
3/4
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 36 times.
✓ Branch 3 taken 6 times.
42 if (r->isError()) {
990
4/8
✓ Branch 0 taken 36 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 36 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 36 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 36 times.
✗ Branch 7 not taken.
72 return deprecated(makeStringResponse(r->get(), 500, "text/plain"));
991 }
992
4/8
✓ 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.
✓ Branch 6 taken 6 times.
✗ Branch 7 not taken.
12 return deprecated(makeStringResponse(r->get(), 200, "text/plain"));
993 } else {
994 ✗ return deprecated(makeStringResponse("Timeout running command", 500, "text/plain"));
995 }
996 42 }
997
4/8
✓ Branch 0 taken 85 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 85 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 85 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 85 times.
✗ Branch 7 not taken.
170 WarningHolder::AddWarningTimeout(900, 61, "No command found for \"" + command + "\" - it may have come from a plugin that is no longer installed or was updated");
998
3/6
✓ Branch 0 taken 85 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 85 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 85 times.
✗ Branch 5 not taken.
340 return makeStringResponse("Not Found", 404, "text/plain");
999 127 }
1000
3/6
✓ Branch 0 taken 175 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 175 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 175 times.
✗ Branch 5 not taken.
700 return makeStringResponse("Not Found", 404, "text/plain");
1001 375 }
1002
1003 /**
1004 * Run a command described by the posted JSON object (with `command` and `args`).
1005 *
1006 * @route POST /api/command
1007 * @body {"command": "Volume Set", "args": ["50"]}
1008 * @response 200 Command result.
1009 * @response 500 The command errored or timed out.
1010 */
1011
1012 /**
1013 * Run a named command, passing its arguments as a JSON array in the body.
1014 *
1015 * An argument may contain `%VAR:name%` to substitute a User Variable's current
1016 * value, as anywhere else a command's arguments are given. No encoding is
1017 * needed here, unlike the GET form above. An unset variable substitutes as an
1018 * empty string.
1019 *
1020 * @route POST /api/command/{command}
1021 * @body ["arg1", "%VAR:brightness%"]
1022 * @response 200 Command result.
1023 * @response 500 The command errored or timed out.
1024 */
1025 190279 HttpResponsePtr CommandManager::render_POST(const HttpRequestPtr& req) {
1026
2/4
✓ Branch 0 taken 190279 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 190279 times.
✗ Branch 3 not taken.
190279 auto parts = getPathPieces(req->path());
1027
1/2
✓ Branch 0 taken 190279 times.
✗ Branch 1 not taken.
190279 std::string p1 = parts[0];
1028
2/4
✓ Branch 0 taken 190279 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 190279 times.
✗ Branch 3 not taken.
190279 if (p1 == "command") {
1029
2/2
✓ Branch 0 taken 258 times.
✓ Branch 1 taken 190021 times.
190279 if (parts.size() > 1) {
1030
1/2
✓ Branch 0 taken 258 times.
✗ Branch 1 not taken.
258 std::string command = parts[1];
1031
1/2
✓ Branch 0 taken 258 times.
✗ Branch 1 not taken.
258 std::string content(getRequestContent(req));
1032
1/2
✓ Branch 0 taken 258 times.
✗ Branch 1 not taken.
258 Json::Value val;
1033 // A body that does not parse used to yield an empty Json::Value and
1034 // the command then ran with zero arguments, which is how a
1035 // form-urlencoded POST ("name=...&startItem=0&...") reached
1036 // "Insert Playlist Immediate" with nothing to insert. Tell the
1037 // caller their body was wrong rather than running something else.
1038
8/8
✓ Branch 0 taken 224 times.
✓ Branch 1 taken 34 times.
✓ Branch 2 taken 207 times.
✓ Branch 3 taken 17 times.
✓ Branch 4 taken 52 times.
✓ Branch 5 taken 155 times.
✓ Branch 6 taken 52 times.
✓ Branch 7 taken 189 times.
258 if (!content.empty() && !LoadJsonFromString(content, val)) {
1039
3/6
✓ 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.
208 return makeStringResponse("Body is not valid JSON - expected an array of arguments", 400, "text/plain");
1040 }
1041 189 std::vector<std::string> args;
1042 189 std::string err;
1043
3/4
✓ Branch 0 taken 189 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 102 times.
✓ Branch 3 taken 87 times.
189 if (!JsonArgsToStrings(val, args, err)) {
1044
3/6
✓ Branch 0 taken 102 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 102 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 102 times.
✗ Branch 5 not taken.
408 return makeStringResponse("Body must be a JSON array of arguments", 400, "text/plain");
1045 }
1046 // Pinned: removeCommand() waits for it, so it cannot be freed mid-run.
1047
1/2
✓ Branch 0 taken 87 times.
✗ Branch 1 not taken.
87 std::shared_ptr<Command> pinned = acquireCommand(command);
1048
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 85 times.
87 if (pinned) {
1049 // Direct Command call, so substitute here - same reason as the
1050 // GET route above.
1051
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 ResolveCommandArgs(pinned.get(), args);
1052 // Direct Command call, not CommandManager::run() - log the
1053 // args here too, same as the GET route above.
1054
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 LogInfo(VB_COMMAND, "POST /api/command/%s from %s: running command \"%s(%s)\"\n",
1055 command.c_str(), getEffectiveClientIP(req).c_str(),
1056 command.c_str(), FormatArgsForLog(args).c_str());
1057
1058 // Publish MQTT event for command execution
1059
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 Json::Value payload;
1060
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 payload["command"] = command;
1061
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 payload["args"] = Json::Value(Json::arrayValue);
1062
2/2
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 2 times.
16 for (const auto& arg : args) {
1063
3/6
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 14 times.
✗ Branch 5 not taken.
14 payload["args"].append(arg);
1064 }
1065
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 payload["trigger"] = "api-post";
1066
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
4 std::string topic = "command/run";
1067
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 std::string payloadStr = SaveJsonToString(payload);
1068
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 LogExcess(VB_COMMAND, "POST MQTT Publishing command: %s, payload: %s\n", topic.c_str(), TruncateForLog(payloadStr, 2000).c_str());
1069
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 Events::Publish(topic, payloadStr);
1070
1071
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 std::unique_ptr<Command::Result> r = invokeCommand(pinned.get(), args);
1072 2 int count = 0;
1073
3/8
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 2 times.
2 while (!r->isDone() && count < 1000) {
1074 ✗ std::this_thread::sleep_for(std::chrono::milliseconds(1));
1075 ✗ count++;
1076 }
1077
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 if (r->isDone()) {
1078
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 2 times.
2 if (r->isError()) {
1079 ✗ return makeStringResponse(r->get(), 500, r->contentType());
1080 }
1081
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 return makeStringResponse(r->get(), 200, r->contentType());
1082 } else {
1083 ✗ return makeStringResponse("Timeout running command", 500, "text/plain");
1084 }
1085 2 }
1086
4/8
✓ Branch 0 taken 85 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 85 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 85 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 85 times.
✗ Branch 7 not taken.
85 WarningHolder::AddWarningTimeout(900, 61, "No command found for \"" + command + "\" - it may have come from a plugin that is no longer installed or was updated");
1087
12/12
✓ Branch 0 taken 85 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 85 times.
✓ Branch 3 taken 104 times.
✓ Branch 4 taken 85 times.
✓ Branch 5 taken 104 times.
✓ Branch 6 taken 85 times.
✓ Branch 7 taken 156 times.
✓ Branch 8 taken 85 times.
✓ Branch 9 taken 156 times.
✓ Branch 10 taken 85 times.
✓ Branch 11 taken 156 times.
814 } else {
1088
1/2
✓ Branch 0 taken 190021 times.
✗ Branch 1 not taken.
190021 std::string command(getRequestContent(req));
1089
2/4
✓ Branch 0 taken 190021 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 190021 times.
✗ Branch 3 not taken.
190021 LogExcess(VB_COMMAND, "Received command: \"%s\"\n", TruncateForLog(command, 2000).c_str());
1090
2/2
✓ Branch 0 taken 190019 times.
✓ Branch 1 taken 2 times.
190021 Json::Value val = LoadJsonFromString(command);
1091
9/12
✓ Branch 0 taken 190019 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 189996 times.
✓ Branch 3 taken 23 times.
✓ Branch 4 taken 189996 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 189996 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 8 times.
✓ Branch 9 taken 189988 times.
✓ Branch 10 taken 31 times.
✓ Branch 11 taken 189988 times.
190019 if (!val.isObject() || !val["command"].isString()) {
1092
3/6
✓ Branch 0 taken 31 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 31 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 31 times.
✗ Branch 5 not taken.
124 return makeStringResponse("Body must be a JSON object with a \"command\" string", 400, "text/plain");
1093 }
1094
4/8
✓ Branch 0 taken 189988 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 189988 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 189988 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 189988 times.
✗ Branch 7 not taken.
189988 LogDebug(VB_COMMAND, "POST /api/command (bare JSON body) from %s: running command \"%s\"\n",
1095 getEffectiveClientIP(req).c_str(), val["command"].asString().c_str());
1096
1/2
✓ Branch 0 taken 189988 times.
✗ Branch 1 not taken.
189988 std::unique_ptr<Command::Result> r = run(val);
1097 189988 int count = 0;
1098
6/8
✓ Branch 0 taken 190098 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 110 times.
✓ Branch 3 taken 189988 times.
✓ Branch 4 taken 110 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 110 times.
✓ Branch 7 taken 189988 times.
190098 while (!r->isDone() && count < 1000) {
1099
1/2
✓ Branch 0 taken 110 times.
✗ Branch 1 not taken.
110 std::this_thread::sleep_for(std::chrono::milliseconds(1));
1100 110 count++;
1101 }
1102
2/4
✓ Branch 0 taken 189988 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 189988 times.
✗ Branch 3 not taken.
189988 if (r->isDone()) {
1103
3/4
✓ Branch 0 taken 189988 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 116 times.
✓ Branch 3 taken 189872 times.
189988 if (r->isError()) {
1104
3/6
✓ Branch 0 taken 116 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 116 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 116 times.
✗ Branch 5 not taken.
116 return makeStringResponse(r->get(), 500, r->contentType());
1105 }
1106
3/6
✓ Branch 0 taken 189872 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 189872 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 189872 times.
✗ Branch 5 not taken.
189872 return makeStringResponse(r->get(), 200, r->contentType());
1107 } else {
1108 ✗ return makeStringResponse("Timeout running command", 500, "text/plain");
1109 }
1110 190021 }
1111
3/6
✓ Branch 0 taken 85 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 85 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 85 times.
✗ Branch 5 not taken.
340 return makeStringResponse("Not Found", 404, "text/plain");
1112 }
1113 ✗ return makeStringResponse("Not Found", 404, "text/plain");
1114 190298 }
1115
1116 // %VAR:name% - substitute a named User Variable's current value. Explicit
1117 // "VAR:" prefix (rather than bare %name%) avoids colliding with unrelated
1118 // existing usages of %word% patterns that happen to match a variable name.
1119 // Applied centrally in CommandManager::run() (see the forward declaration
1120 // near the top of this file) rather than at each call site, so every way a
1121 // command can run - api/command, a preset trigger, GPIO, MultiSync, a
1122 // playlist "FPP Command" entry, If, a recurring task, ... - resolves it the
1123 // same way. Kept out of the shared ReplaceKeywords() in common.cpp so the
1124 // Variables dependency doesn't spread into the small standalone CLI helpers
1125 // that link common.o directly (fpp, fppoled, fsequtils).
1126 437227 static std::string ReplaceVariableKeywords(std::string str) {
1127 437227 std::size_t varPos = 0;
1128
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 437227 times.
437231 while ((varPos = str.find("%VAR:", varPos)) != std::string::npos) {
1129 4 std::size_t varEnd = str.find('%', varPos + 5);
1130
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 if (varEnd == std::string::npos) {
1131 ✗ break;
1132 }
1133
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 std::string varName = str.substr(varPos + 5, varEnd - varPos - 5);
1134
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 std::string varValue = Variables::INSTANCE.getVariable(varName, "");
1135
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 str.replace(varPos, varEnd - varPos + 1, varValue);
1136 4 varPos += varValue.length();
1137 4 }
1138 437227 return str;
1139 }
1140
1141 11 static void ReplaceVariableKeywordsInJson(Json::Value& v) {
1142
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 3 times.
11 if (v.isString()) {
1143
3/6
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 8 times.
✗ Branch 5 not taken.
8 v = ReplaceVariableKeywords(v.asString());
1144
4/6
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
3 } else if (v.isArray() || v.isObject()) {
1145
7/12
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 10 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 10 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 13 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 10 times.
✓ Branch 11 taken 3 times.
13 for (auto& c : v) {
1146
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 ReplaceVariableKeywordsInJson(c);
1147 }
1148 }
1149 11 }
1150
1151 // %VAR: substitution for every arg of a command about to run. Most args are
1152 // text and take the value as-is. A condition list or command list (If's) is
1153 // JSON text, though, and pasting a value into that - one containing a quote,
1154 // say - broke the JSON, so If silently ran an empty list. Those are
1155 // substituted inside their string values and serialized again instead.
1156 194256 static void ResolveCommandArgs(const Command* cmd, std::vector<std::string>& args) {
1157 194256 auto decl = cmd->args.begin();
1158
2/2
✓ Branch 0 taken 436883 times.
✓ Branch 1 taken 194256 times.
631139 for (size_t i = 0; i < args.size(); i++) {
1159
2/2
✓ Branch 0 taken 417441 times.
✓ Branch 1 taken 19442 times.
854324 bool structured = decl != cmd->args.end() &&
1160
6/8
✓ Branch 0 taken 417441 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 415425 times.
✓ Branch 3 taken 2016 times.
✓ Branch 4 taken 415425 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1008 times.
✓ Branch 7 taken 414417 times.
417441 (decl->type == "commandlist" || decl->type == "conditionlist");
1161
1/2
✓ Branch 0 taken 436883 times.
✗ Branch 1 not taken.
436883 Json::Value v;
1162
8/10
✓ Branch 0 taken 3024 times.
✓ Branch 1 taken 433859 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 3023 times.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1 times.
✓ Branch 9 taken 436882 times.
436884 if (structured && args[i].find("%VAR:") != std::string::npos && LoadJsonFromString(args[i], v) &&
1163
2/8
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1 times.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
1 (v.isArray() || v.isObject())) {
1164
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 ReplaceVariableKeywordsInJson(v);
1165
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
2 args[i] = SaveJsonToString(v);
1166 } else {
1167
2/4
✓ Branch 0 taken 436882 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 436882 times.
✗ Branch 3 not taken.
436882 args[i] = ReplaceVariableKeywords(args[i]);
1168 }
1169
2/2
✓ Branch 0 taken 417441 times.
✓ Branch 1 taken 19442 times.
436883 if (decl != cmd->args.end()) {
1170 417441 ++decl;
1171 }
1172 436883 }
1173 194256 }
1174
1175 // %VAR: substitution isn't done here - it's applied once, centrally, by
1176 // CommandManager::run() itself (see ReplaceVariableKeywords above), which
1177 // every caller of this function's result eventually goes through.
1178 120 Json::Value CommandManager::ReplaceCommandKeywords(Json::Value cmd, std::map<std::string, std::string>& keywords) {
1179
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 120 times.
120 if (!cmd.isMember("args"))
1180 ✗ return cmd;
1181
1182
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 120 times.
120 if (!cmd["args"].isArray())
1183 ✗ return cmd;
1184
2/2
✓ Branch 0 taken 666 times.
✓ Branch 1 taken 120 times.
786 for (Json::ArrayIndex i = 0; i < cmd["args"].size(); i++) {
1185 // Only text has keywords in it; an object or array arg is passed on
1186 // as JSON by run(), and asString() would throw on it here.
1187
1/2
✓ Branch 0 taken 666 times.
✗ Branch 1 not taken.
666 if (cmd["args"][i].isString()) {
1188
7/14
✓ Branch 0 taken 666 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 666 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 666 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 666 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 666 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 666 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 666 times.
✗ Branch 13 not taken.
666 cmd["args"][i] = ReplaceKeywords(cmd["args"][i].asString(), keywords);
1189 }
1190 }
1191
1192 120 return cmd;
1193 }
1194
1195 // A preset whose own commands fire the event that triggers it - a MEDIA_STARTED
1196 // preset that runs Play Media, which fires MEDIA_STARTED again - recursed on
1197 // the calling thread without bound. Each level started and stopped another
1198 // pipeline; thousands of nested calls deep, a second request racing the chain
1199 // for the same stream slot hit a freed object. Run a preset at most once per
1200 // call chain: a nested trigger of a preset already running on this thread is
1201 // skipped. Triggers from other threads, or later from the same thread (a
1202 // MEDIA_STOPPED preset restarting media when it ends), are unaffected.
1203 static constexpr int WARNING_ID_PRESET_RECURSION = 66;
1204
1205 class PresetRecursionGuard {
1206 public:
1207 111 explicit PresetRecursionGuard(const std::string& name) :
1208
1/2
✓ Branch 0 taken 111 times.
✗ Branch 1 not taken.
111 m_name(name), m_entered(running().insert(name).second) {
1209
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 108 times.
111 if (!m_entered) {
1210
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 LogWarn(VB_COMMAND, "Command Preset \"%s\" triggered itself while it was still running; skipping the nested run\n", name.c_str());
1211
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
12 WarningHolder::AddWarningTimeout(900, WARNING_ID_PRESET_RECURSION,
1212
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
6 "Command Preset \"" + name + "\" triggers itself, so its nested run was skipped");
1213 }
1214 111 }
1215 111 ~PresetRecursionGuard() {
1216
2/2
✓ Branch 0 taken 108 times.
✓ Branch 1 taken 3 times.
111 if (m_entered) {
1217 108 running().erase(m_name);
1218 }
1219 111 }
1220 111 bool entered() const { return m_entered; }
1221
1222 private:
1223 219 static std::set<std::string>& running() {
1224
2/2
✓ Branch 0 taken 64 times.
✓ Branch 1 taken 155 times.
219 static thread_local std::set<std::string> names;
1225 219 return names;
1226 }
1227 std::string m_name;
1228 bool m_entered;
1229 };
1230
1231 11 int CommandManager::TriggerPreset(int slot, std::map<std::string, std::string>& keywords) {
1232
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 11 times.
11 if (slot < 1) {
1233 ✗ return 0; // 0 is "no slot": never run every unslotted preset
1234 }
1235
1/2
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
11 std::unique_lock<std::mutex> lock(presetsMutex);
1236 11 std::list<Json::Value> slotPresets;
1237 11 std::list<std::string> slotPresetNames;
1238
3/4
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 55 times.
✓ Branch 3 taken 11 times.
66 for (auto const& name : presets.getMemberNames()) {
1239
4/6
✓ Branch 0 taken 121 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 121 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 66 times.
✓ Branch 5 taken 55 times.
121 for (int i = 0; i < presets[name].size(); i++) {
1240
6/10
✓ Branch 0 taken 66 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 66 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 66 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 66 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 16 times.
✓ Branch 9 taken 50 times.
66 if (presets[name][i]["presetSlot"].asInt() == slot) {
1241
4/8
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 16 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 16 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 16 times.
✗ Branch 7 not taken.
16 Json::Value cmd = ReplaceCommandKeywords(presets[name][i], keywords);
1242
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 slotPresets.push_back(cmd);
1243
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 slotPresetNames.push_back(name);
1244 16 }
1245 }
1246 11 }
1247
1/2
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
11 lock.unlock();
1248
1249 // Publish MQTT event for preset slot trigger
1250
1/2
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
11 Json::Value payload;
1251
2/4
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 11 times.
✗ Branch 3 not taken.
11 payload["slot"] = slot;
1252
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 11 times.
11 if (!keywords.empty()) {
1253 ✗ payload["keywords"] = Json::Value(Json::objectValue);
1254 ✗ for (const auto& kv : keywords) {
1255 ✗ payload["keywords"][kv.first] = kv.second;
1256 }
1257 }
1258
1/2
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
22 std::string topic = "command/preset/triggered";
1259
3/6
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 11 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 11 times.
✗ Branch 5 not taken.
11 Events::Publish(topic, SaveJsonToString(payload));
1260
1261 11 auto nameIt = slotPresetNames.begin();
1262
2/2
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 11 times.
27 for (auto const& preset : slotPresets) {
1263
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 PresetRecursionGuard guard(*nameIt);
1264
2/2
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 2 times.
16 if (guard.entered()) {
1265
3/6
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 14 times.
✗ Branch 5 not taken.
14 LogDebug(VB_COMMAND, "Command Preset \"%s\" (slot %d) running command \"%s\"\n", nameIt->c_str(), slot, preset["command"].asString().c_str());
1266
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
14 run(preset);
1267 }
1268 16 ++nameIt;
1269 16 }
1270
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 9 times.
11 return slotPresets.empty() ? 0 : 1;
1271 11 }
1272
1273 11 int CommandManager::TriggerPreset(int slot) {
1274 11 std::map<std::string, std::string> keywords;
1275
1276
1/2
✓ Branch 0 taken 11 times.
✗ Branch 1 not taken.
22 return TriggerPreset(slot, keywords);
1277 11 }
1278
1279 303 int CommandManager::TriggerPreset(std::string name, std::map<std::string, std::string>& keywords) {
1280
1/2
✓ Branch 0 taken 303 times.
✗ Branch 1 not taken.
303 std::unique_lock<std::mutex> lock(presetsMutex);
1281
3/4
✓ Branch 0 taken 303 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 208 times.
✓ Branch 3 taken 95 times.
303 if (!presets.isMember(name)) {
1282
3/4
✓ Branch 0 taken 208 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 95 times.
✓ Branch 3 taken 113 times.
208 if (missingPresets.find(name) == missingPresets.end()) {
1283
1/2
✓ Branch 0 taken 95 times.
✗ Branch 1 not taken.
95 LogWarn(VB_COMMAND, "No preset found for name \"%s\"\n", name.c_str());
1284
1/2
✓ Branch 0 taken 95 times.
✗ Branch 1 not taken.
95 missingPresets.insert(name);
1285 }
1286 208 return 0;
1287 }
1288
1289
2/4
✓ Branch 0 taken 95 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 95 times.
✗ Branch 3 not taken.
95 auto it = presets[name];
1290
1/2
✓ Branch 0 taken 95 times.
✗ Branch 1 not taken.
95 lock.unlock();
1291
1292
1/2
✓ Branch 0 taken 95 times.
✗ Branch 1 not taken.
95 PresetRecursionGuard guard(name);
1293
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 94 times.
95 if (!guard.entered()) {
1294 1 return 0;
1295 }
1296
1297 // Publish MQTT event for preset trigger
1298
1/2
✓ Branch 0 taken 94 times.
✗ Branch 1 not taken.
94 Json::Value payload;
1299
2/4
✓ Branch 0 taken 94 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 94 times.
✗ Branch 3 not taken.
94 payload["preset"] = name;
1300
2/2
✓ Branch 0 taken 60 times.
✓ Branch 1 taken 34 times.
94 if (!keywords.empty()) {
1301
2/4
✓ Branch 0 taken 60 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 60 times.
✗ Branch 3 not taken.
60 payload["keywords"] = Json::Value(Json::objectValue);
1302
2/2
✓ Branch 0 taken 65 times.
✓ Branch 1 taken 60 times.
125 for (const auto& kv : keywords) {
1303
3/6
✓ Branch 0 taken 65 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 65 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 65 times.
✗ Branch 5 not taken.
65 payload["keywords"][kv.first] = kv.second;
1304 }
1305 }
1306
1/2
✓ Branch 0 taken 94 times.
✗ Branch 1 not taken.
188 std::string topic = "command/preset/triggered";
1307
3/6
✓ Branch 0 taken 94 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 94 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 94 times.
✗ Branch 5 not taken.
94 Events::Publish(topic, SaveJsonToString(payload));
1308
1309
3/4
✓ Branch 0 taken 198 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 104 times.
✓ Branch 3 taken 94 times.
198 for (int i = 0; i < it.size(); i++) {
1310
3/6
✓ Branch 0 taken 104 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 104 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 104 times.
✗ Branch 5 not taken.
104 Json::Value cmd = ReplaceCommandKeywords(it[i], keywords);
1311
3/6
✓ Branch 0 taken 104 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 104 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 104 times.
✗ Branch 5 not taken.
104 LogDebug(VB_COMMAND, "Command Preset \"%s\" running command \"%s\"\n", name.c_str(), cmd["command"].asString().c_str());
1312
1/2
✓ Branch 0 taken 104 times.
✗ Branch 1 not taken.
104 run(cmd);
1313 104 }
1314
1315 94 return 1;
1316 303 }
1317
1318 148 int CommandManager::TriggerPreset(std::string name) {
1319 148 std::map<std::string, std::string> keywords;
1320
1321
2/4
✓ Branch 0 taken 148 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 148 times.
✗ Branch 3 not taken.
296 return TriggerPreset(name, keywords);
1322 148 }
1323
1324 // The preset file's modification time in nanoseconds, 0 if it is missing.
1325 // Whole seconds were not enough: a save in the same second as the previous
1326 // load (two quick edits, or an edit right after startup) looked unchanged, and
1327 // the presets stayed stale until the next save.
1328 1906 static uint64_t PresetFileStamp(const std::string& file) {
1329 struct stat st;
1330
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1904 times.
1906 if (stat(file.c_str(), &st) != 0) {
1331 2 return 0;
1332 }
1333 #ifdef PLATFORM_OSX
1334 const struct timespec& ts = st.st_mtimespec;
1335 #else
1336 1904 const struct timespec& ts = st.st_mtim;
1337 #endif
1338 1904 return (uint64_t)ts.tv_sec * 1000000000ULL + (uint64_t)ts.tv_nsec;
1339 }
1340
1341 10 void CommandManager::MaybeReloadPresets() {
1342
3/6
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 10 times.
✗ Branch 5 not taken.
10 std::string commandsFile = FPP_DIR_CONFIG("/commandPresets.json");
1343 // Any change, not only a newer time: a restored backup can carry an
1344 // older modification time than the file it replaces.
1345
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 if (lastPresetTimeStamp != PresetFileStamp(commandsFile)) {
1346
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 LoadPresets();
1347 }
1348 10 }
1349
1350 1898 void CommandManager::LoadPresets(bool atStartup) {
1351
1/2
✓ Branch 0 taken 1898 times.
✗ Branch 1 not taken.
1898 LogDebug(VB_COMMAND, "Loading Command Presets\n");
1352
1353 char id[6];
1354 1898 memset(id, 0, sizeof(id));
1355
1356
3/6
✓ Branch 0 taken 1898 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1898 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1898 times.
✗ Branch 5 not taken.
1898 std::string commandsFile = FPP_DIR_CONFIG("/commandPresets.json");
1357
1358
1/2
✓ Branch 0 taken 1898 times.
✗ Branch 1 not taken.
1898 Json::Value allCommands;
1359
1360
3/4
✓ Branch 0 taken 1898 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 159 times.
✓ Branch 3 taken 1739 times.
1898 if (FileExists(commandsFile)) {
1361 // Load new config file
1362 // Stamped before the read, so an edit landing during it is seen as a
1363 // change by the next check rather than recorded as already loaded.
1364 159 lastPresetTimeStamp = PresetFileStamp(commandsFile);
1365
1/2
✓ Branch 0 taken 159 times.
✗ Branch 1 not taken.
159 allCommands = LoadJsonFromFile(commandsFile, JsonRoot::Object);
1366
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1737 times.
1739 } else if (!atStartup) {
1367 // Deleted while running (Reset Config's Configuration area does it):
1368 // no presets, and no file written back. The old events were
1369 // converted when fppd started, and writing an empty file here put
1370 // back what the reset had just removed.
1371 2 lastPresetTimeStamp = 0;
1372 } else {
1373 // Convert any old events to new format
1374
1/2
✓ Branch 0 taken 1737 times.
✗ Branch 1 not taken.
1737 Json::Value commands(Json::arrayValue);
1375
1376
2/2
✓ Branch 0 taken 43425 times.
✓ Branch 1 taken 1737 times.
45162 for (int major = 1; major <= 25; major++) {
1377
2/2
✓ Branch 0 taken 1085625 times.
✓ Branch 1 taken 43425 times.
1129050 for (int minor = 1; minor <= 25; minor++) {
1378 1085625 snprintf(id, sizeof(id), "%02d_%02d", major, minor);
1379
4/8
✓ Branch 0 taken 1085625 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1085625 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1085625 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1085625 times.
✗ Branch 7 not taken.
1085625 std::string filename = FPP_DIR_MEDIA(std::string("/events/") + id + ".fevt");
1380
1381
2/4
✓ Branch 0 taken 1085625 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1085625 times.
1085625 if (FileExists(filename)) {
1382 ✗ LogDebug(VB_COMMAND, "Converting old %s event to a Command Preset\n", id);
1383
1384 ✗ Json::Value event;
1385 ✗ if (LoadJsonFromFile(filename, event, JsonRoot::Object)) {
1386 ✗ event.removeMember("majorId");
1387 ✗ event.removeMember("minorId");
1388 ✗ event["presetSlot"] = 0;
1389
1390 ✗ commands.append(event);
1391 }
1392 ✗ }
1393 1085625 }
1394 }
1395
1396
2/4
✓ Branch 0 taken 1737 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1737 times.
✗ Branch 3 not taken.
1737 allCommands["commands"] = commands;
1397
2/4
✓ Branch 0 taken 1737 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1737 times.
✗ Branch 3 not taken.
1737 SaveJsonToFile(allCommands, commandsFile, "\t");
1398 1737 lastPresetTimeStamp = PresetFileStamp(commandsFile);
1399 1737 }
1400
1401 // Built aside and swapped in under presetsMutex: TriggerPreset() reads
1402 // `presets` under that lock from HTTP, MQTT and main-loop threads, and a
1403 // reload used to clear and refill it in place with no lock at all.
1404 //
1405 // The file can be hand-edited or uploaded through the config file API, so
1406 // every entry is checked here, once, rather than by asString()/asInt() at
1407 // trigger time - where the wrong type threw on whichever thread fired the
1408 // preset, and here at startup threw out of Init() on every boot.
1409
1/2
✓ Branch 0 taken 1898 times.
✗ Branch 1 not taken.
1898 Json::Value loaded(Json::objectValue);
1410
5/8
✓ Branch 0 taken 1898 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1896 times.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 1896 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
1898 const Json::Value& list = allCommands.isObject() ? allCommands["commands"] : Json::Value::nullSingleton();
1411
8/10
✓ Branch 0 taken 1898 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1895 times.
✓ Branch 3 taken 3 times.
✓ Branch 4 taken 1895 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✓ Branch 7 taken 1894 times.
✓ Branch 8 taken 1 times.
✓ Branch 9 taken 1897 times.
1898 if (!list.isNull() && !list.isArray()) {
1412
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogErr(VB_COMMAND, "commandPresets.json: \"commands\" is not an array - no presets loaded\n");
1413 }
1414
8/10
✓ Branch 0 taken 2071 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2067 times.
✓ Branch 3 taken 4 times.
✓ Branch 4 taken 2067 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 173 times.
✓ Branch 7 taken 1894 times.
✓ Branch 8 taken 173 times.
✓ Branch 9 taken 1898 times.
2071 for (Json::ArrayIndex i = 0; list.isArray() && i < list.size(); i++) {
1415
2/4
✓ Branch 0 taken 173 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 173 times.
✗ Branch 3 not taken.
173 Json::Value entry = list[i];
1416
9/16
✓ Branch 0 taken 171 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 171 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 170 times.
✓ Branch 5 taken 1 times.
✓ Branch 6 taken 170 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 170 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 170 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 3 times.
✓ Branch 13 taken 170 times.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
344 if (!entry.isObject() || !entry["name"].isString() || entry["name"].asString().empty() ||
1417
14/24
✓ Branch 0 taken 173 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 171 times.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 170 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 170 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 170 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 170 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 170 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 170 times.
✗ Branch 15 not taken.
✓ Branch 16 taken 170 times.
✗ Branch 17 not taken.
✓ Branch 18 taken 170 times.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
✓ Branch 21 taken 170 times.
✓ Branch 22 taken 170 times.
✓ Branch 23 taken 3 times.
344 !entry["command"].isString() || (!entry["args"].isNull() && !entry["args"].isArray())) {
1418
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 LogWarn(VB_COMMAND, "commandPresets.json: skipping entry %d - it needs a text \"name\" and \"command\" and an \"args\" array\n", (int)i);
1419 3 continue;
1420 }
1421
3/6
✓ Branch 0 taken 170 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 170 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 170 times.
170 if (!entry["presetSlot"].isConvertibleTo(Json::intValue)) {
1422 ✗ entry["presetSlot"] = 0;
1423 }
1424
2/4
✓ Branch 0 taken 170 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 170 times.
✗ Branch 3 not taken.
170 std::string name = entry["name"].asString();
1425
3/4
✓ Branch 0 taken 170 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 161 times.
✓ Branch 3 taken 9 times.
170 if (!loaded.isMember(name)) {
1426
2/4
✓ Branch 0 taken 161 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 161 times.
✗ Branch 3 not taken.
161 loaded[name] = Json::Value(Json::arrayValue);
1427 }
1428
2/4
✓ Branch 0 taken 170 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 170 times.
✗ Branch 3 not taken.
170 loaded[name].append(entry);
1429
2/2
✓ Branch 0 taken 170 times.
✓ Branch 1 taken 3 times.
173 }
1430
1/2
✓ Branch 0 taken 1898 times.
✗ Branch 1 not taken.
1898 std::unique_lock<std::mutex> lock(presetsMutex);
1431
1/2
✓ Branch 0 taken 1898 times.
✗ Branch 1 not taken.
1898 presets.swap(loaded);
1432 1898 missingPresets.clear();
1433 1898 }
1434
1435 17462 bool CommandManager::HasPreset(const std::string& name) {
1436
1/2
✓ Branch 0 taken 17462 times.
✗ Branch 1 not taken.
17462 std::unique_lock<std::mutex> lock(presetsMutex);
1437
1/2
✓ Branch 0 taken 17462 times.
✗ Branch 1 not taken.
34924 return presets.isMember(name);
1438 17462 }
1439
1440 1745 Json::Value CommandManager::GetPresets() {
1441
1/2
✓ Branch 0 taken 1745 times.
✗ Branch 1 not taken.
1745 std::unique_lock<std::mutex> lock(presetsMutex);
1442
1/2
✓ Branch 0 taken 1745 times.
✗ Branch 1 not taken.
3490 return presets;
1443 1745 }
1444