GCC Code Coverage Report


Directory: ./
File: commands/Commands.h
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 46 46 100.0%
Functions: 21 21 100.0%
Branches: 5 8 62.5%

Line Branch Exec Source
1 #pragma once
2 /*
3 * This file is part of the Falcon Player (FPP) and is Copyright (C)
4 * 2013-2022 by the Falcon Player Developers.
5 *
6 * The Falcon Player (FPP) is free software, and is covered under
7 * multiple Open Source licenses. Please see the included 'LICENSES'
8 * file for descriptions of what files are covered by each license.
9 *
10 * This source file is covered under the LGPL v2.1 as described in the
11 * included LICENSE.LGPL file.
12 */
13
14 #include <atomic>
15 #include <condition_variable>
16 #include "fpp-json.h"
17 #include "fpphttp_types.h"
18 #include <list>
19 #include <map>
20 #include <memory>
21 #include <set>
22 #include <shared_mutex>
23 #include <span>
24 #include <string>
25 #include <vector>
26
27 class Command {
28 public:
29 Command(const std::string& n);
30 Command(const std::string& n, const std::string& descript);
31 virtual ~Command();
32
33 virtual Json::Value getDescription();
34 3837 virtual bool hidden() const { return false; }
35
36 // ABI RULE: do NOT add new virtual methods to Command, and do NOT make any
37 // Command method call a virtual that older objects might lack. External
38 // channel-output/event plugins subclass Command and are compiled separately
39 // against these headers; inserting a virtual shifts the vtable so FPP
40 // dispatches a base-class call into the wrong slot of a pre-built plugin's
41 // object. (A mid-vtable disallowMultisync() once did exactly this:
42 // Command::getDescription() ended up calling the plugin's run() -> crash.)
43 // To add per-command metadata, emit it from an override of the existing
44 // getDescription() virtual, ideally via an FPP-internal intermediate class
45 // (see LocalOnlyCommand) that plugins never subclass.
46
47 // Result and ErrorResult are entirely inline, so every plugin that returns
48 // one builds its own copy of their vtable and layout, and FPP then calls
49 // through it. The size fingerprints below cover only Command and
50 // CommandArg: any member or virtual change here needs an
51 // FPP_PLUGIN_API_VERSION bump (Plugin.h).
52 class Result {
53 public:
54 39 Result() :
55
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
117 m_contentType("text/plain") {}
56 194189 Result(const std::string& r) :
57 194189 m_result(r),
58
1/2
✓ Branch 0 taken 194189 times.
✗ Branch 1 not taken.
582567 m_contentType("text/plain") {}
59 776574 virtual ~Result() {}
60
61 388857 virtual bool isDone() { return true; }
62 192944 virtual bool isError() { return false; }
63 190055 virtual const std::string& get() { return m_result; }
64 189990 virtual const std::string& contentType() { return m_contentType; }
65
66 protected:
67 std::string m_result;
68 std::string m_contentType;
69 };
70 class ErrorResult : public Result {
71 public:
72 ErrorResult() :
73 Result() {}
74 164 ErrorResult(const std::string& r) :
75 164 Result(r) {}
76 656 virtual ~ErrorResult() {}
77 155 virtual bool isError() override { return true; }
78 };
79
80 virtual std::unique_ptr<Result> run(const std::vector<std::string>& args) = 0;
81
82 // LAYOUT RULE: sizeof(CommandArg) is frozen as of plugin API version 5.
83 //
84 // Plugins build this type and push_back it onto Command::args; std::list
85 // instantiates the node (sizeof(CommandArg) + two pointers) inside the
86 // *plugin*, while FPP walks and destroys those nodes with its own idea of
87 // the size. Every past mismatch corrupted the heap in the field. So: do NOT
88 // add, remove, or reorder a data member below. Anything new goes in Ext,
89 // which lives in Commands.cpp and can grow freely because plugins only ever
90 // see a pointer to it. fpp_command_arg_abi_size() (bottom of this file)
91 // enforces this at plugin load, so a slip here refuses to load rather than
92 // corrupting memory - but it is a backstop, not a licence.
93 class CommandArg {
94 public:
95 // Out-of-line for the same reason as the special members below: even
96 // though it never touches ext, the compiler still needs ~unique_ptr<Ext>
97 // here to unwind if a later member's construction throws.
98 CommandArg(const std::string& n, const std::string& t, const std::string& d, bool o = false);
99
100 // Out-of-line so that Ext stays incomplete here. This is load-bearing
101 // twice over: an implicitly-generated one would fail to compile in any
102 // TU that lacks Ext, and routing them through libfpp is what keeps a
103 // plugin's std::list<CommandArg> teardown on FPP's implementation
104 // rather than its own possibly-stale copy.
105 CommandArg(const CommandArg& o);
106 CommandArg(CommandArg&& o) noexcept;
107 ~CommandArg();
108 // Already implicitly deleted by the const members below; stated so a
109 // later edit that drops the const does not silently resurrect them
110 // with an Ext-aliasing shallow copy.
111 CommandArg& operator=(const CommandArg&) = delete;
112 CommandArg& operator=(CommandArg&&) = delete;
113
114 1862555 CommandArg& setRange(int mn, int mx) {
115 1862555 min = mn;
116 1862555 max = mx;
117 1862555 return *this;
118 }
119 77341 CommandArg& setContentListUrl(const std::string& s, bool ab = false) {
120 77341 contentListUrl = s;
121 77341 allowBlanks = ab;
122 77341 return *this;
123 }
124 35295 CommandArg& setContentList(std::vector<std::string> v) {
125 35295 contentList = std::move(v);
126 35295 return *this;
127 }
128 961860 CommandArg& setContentListRange(std::span<const std::string_view> s) {
129 961860 contentList.clear();
130 961860 contentList.reserve(s.size());
131
2/2
✓ Branch 0 taken 37423392 times.
✓ Branch 1 taken 961860 times.
38385252 for (auto& e : s)
132
1/2
✓ Branch 0 taken 37423392 times.
✗ Branch 1 not taken.
37423392 contentList.emplace_back(e);
133 961860 return *this;
134 }
135 3895003 CommandArg& setDefaultValue(std::string_view d) {
136 3895003 defaultValue = d;
137 3895003 return *this;
138 }
139 1888 CommandArg& setGetAdjustableValueURL(const std::string& g) {
140 1888 adjustableGetValueURL = g;
141 1888 adjustable = true;
142 1888 return *this;
143 }
144 12243 CommandArg& setAdjustable() {
145 12243 adjustable = true;
146 12243 return *this;
147 }
148 // ---- Ext-backed fields ------------------------------------------
149 // Everything below is stored in Ext (Commands.cpp), not in this object,
150 // so it can change freely without moving anyone's members. These are
151 // every field added after plugin API version 3; they all landed between
152 // 2026-07-23 and 2026-07-27, and API version 5 already forces a plugin
153 // rebuild, so this was the moment to get them behind the pointer before
154 // anything depended on their offsets. Add new fields here, never above.
155 // The setter signatures are unchanged, so call sites are untouched.
156
157 // Maps a value of this arg to the names of other args that are only
158 // relevant when this arg has that value (e.g. "type" == "Random" ->
159 // show "min"/"max"). Frontend hides every listed child arg's row
160 // until its parent's value matches, mirroring the same mechanism
161 // already used by the playlist-entry-type editor (fpp.js
162 // UpdateChildVisibility) for CommandArg-driven UIs generally.
163 CommandArg& setChildren(std::map<std::string, std::vector<std::string>> c);
164 const std::map<std::string, std::vector<std::string>>& getChildren() const;
165 // Hides this arg's row unless FPP's global UI Level setting is
166 // "Advanced" (frontend: fpp.js PrintArgInputs already checks
167 // val.advanced, this was simply never set from any Command until now).
168 CommandArg& setAdvanced(bool a = true);
169 bool isAdvanced() const;
170 // Extra explanatory text shown as a hover tooltip next to the label -
171 // for when the short description isn't enough room to explain a
172 // field properly. Reuses the same Bootstrap tooltip mechanism
173 // (data-bs-toggle="tooltip" + .tooltip()) already used elsewhere in
174 // fpp.js (the command-preset preview icon), just newly wired into
175 // the generic per-arg renderer.
176 CommandArg& setHelp(const std::string& h);
177 const std::string& getHelp() const;
178 // Groups this arg under a named heading alongside any other
179 // consecutive args sharing the same section (fpp.js PrintArgInputs -
180 // ported from the playlist-entry-editor's Primary Media / Extra
181 // Media / Entry Properties sections). Purely presentational.
182 CommandArg& setSection(const std::string& s);
183 const std::string& getSection() const;
184 // Renders a 2-option contentList as a Bootstrap btn-check pill toggle
185 // (matching the If command's own Match ALL/ANY condition toggle)
186 // instead of the generic <select> - opt-in only, so every other
187 // contentList-backed arg elsewhere in the app keeps its existing
188 // dropdown rendering unchanged.
189 CommandArg& setToggleStyle(bool t = true);
190 bool isToggleStyle() const;
191 // Overrides the label shown next to a toggleStyle arg's own toggle
192 // (fpp.js renders it inline, e.g. "Order:", matching the condition
193 // editor's own "Match:" label) instead of reusing this arg's
194 // description - the description is still what labels the row this
195 // toggle is rendered directly above (e.g. "Then Run"), so reusing it
196 // here too would just repeat the same text twice.
197 CommandArg& setToggleLabel(const std::string& l);
198 const std::string& getToggleLabel() const;
199
200 // ---- FROZEN AT API VERSION 5 - see the LAYOUT RULE above ----------
201 // This is exactly the plugin API version 3 member set, the last one that
202 // was ever in the field long enough to matter.
203 const std::string name;
204 const std::string type;
205 const std::string description;
206
207 bool optional;
208 std::string contentListUrl;
209 std::vector<std::string> contentList;
210 bool allowBlanks;
211 int min;
212 int max;
213 std::string defaultValue;
214 std::string adjustableGetValueURL;
215 bool adjustable;
216
217 private:
218 // ---- everything new goes here, in Commands.cpp -------------------
219 // Allocated on first use: most args set no extended field, and they
220 // should not pay for one.
221 struct Ext;
222 Ext& mutableExt();
223 std::unique_ptr<Ext> ext;
224 // ---- nothing may follow ext ---------------------------------------
225 };
226
227 // ---- FROZEN AT API VERSION 5 --------------------------------------------
228 // Same rule as CommandArg, and for a sharper reason: plugins *subclass*
229 // Command, so a member added here shifts every member of their derived
230 // class. New base-class state goes in Data (Commands.cpp) instead.
231 // Note sizeof(std::list) does not depend on its element type, so args stays
232 // put no matter how CommandArg's Ext grows.
233 std::string name;
234 std::list<CommandArg> args;
235 std::string description;
236
237 private:
238 struct Data;
239 std::unique_ptr<Data> data; // allocated on first use, like CommandArg::ext
240 // ---- nothing may follow data --------------------------------------------
241 };
242
243 // ABI fingerprints, checked by PluginManager::loadPlugin().
244 //
245 // The Ext/Data pimpls above are what make these two sizes stay put; these are
246 // what prove it. CommandArg grew twice without FPP_PLUGIN_API_VERSION being
247 // bumped (advanced/help/children/toggleStyle/toggleLabel, then section), and
248 // both times the result was heap corruption in the field. Relying on a human to
249 // remember the bump has visibly not worked, so the sizes travel with the header
250 // and the loader compares them.
251 //
252 // Emitted weak + default-visibility so every plugin that includes this header
253 // exports its own compiled-in values; libfpp defines them too, and the loader
254 // uses dladdr() provenance to tell "the plugin disagrees with us" (reject) from
255 // "the plugin never touches commands" (nothing to check). Optional by
256 // construction - a plugin that does not include Commands.h is unaffected.
257 #ifdef __cplusplus
258 extern "C" {
259 8 __attribute__((weak, visibility("default"))) unsigned int fpp_command_arg_abi_size() {
260 8 return (unsigned int)sizeof(Command::CommandArg);
261 }
262 8 __attribute__((weak, visibility("default"))) unsigned int fpp_command_abi_size() {
263 8 return (unsigned int)sizeof(Command);
264 }
265 }
266 #endif
267
268 class CommandManager {
269 public:
270 void Init();
271 void Cleanup();
272
273 void addCommand(Command* cmd);
274 void addCategorizedCommand(Command* cmd, const std::string& category, int level = 0);
275 void removeCommand(Command* cmd);
276 void removeCommand(const std::string& cmdName);
277 // Unregisters cmd and deletes it once no run is inside it: at once when it
278 // is idle, otherwise when its last run returns, however long that takes.
279 // The delete can land after the caller has moved on, so only for commands
280 // whose class lives in libfpp, never in a library that may be unloaded.
281 void retireCommand(Command* cmd);
282
283 // Every currently registered command, name and object. PluginManager diffs
284 // this around a plugin's load to work out which commands that plugin
285 // registered: addCommand() carries no owner and cannot gain one without
286 // breaking every plugin that calls it, so ownership is deduced the same way
287 // addControlCallbacks() deduces a plugin's epoll descriptors.
288 std::vector<std::pair<std::string, Command*>> getRegisteredCommands() const;
289
290 Json::Value getDescriptions();
291 bool HasPreset(const std::string& name);
292 // A copy of the presets as loaded: name -> array of entries, each with a
293 // text "command" and an "args" array (or no args). Entries the file holds
294 // in any other shape were dropped by LoadPresets(), so a reader of this
295 // never has to check the file's shape again.
296 Json::Value GetPresets();
297
298 virtual std::unique_ptr<Command::Result> run(const std::string& command, const std::vector<std::string>& args);
299 virtual std::unique_ptr<Command::Result> run(const std::string& command, const Json::Value& argsArray);
300 virtual std::unique_ptr<Command::Result> run(const Json::Value& command);
301
302 HttpResponsePtr render_GET(const HttpRequestPtr& req);
303 HttpResponsePtr render_POST(const HttpRequestPtr& req);
304
305 // By slot: 1 if a preset in the slot ran, 0 if none is in it (or slot < 1).
306 int TriggerPreset(int slot, std::map<std::string, std::string>& keywords);
307 int TriggerPreset(int slot);
308 int TriggerPreset(std::string name, std::map<std::string, std::string>& keywords);
309 int TriggerPreset(std::string name);
310
311 static CommandManager INSTANCE;
312
313 private:
314 CommandManager();
315 ~CommandManager();
316
317 // atStartup: a missing file is created, from any old events/*.fevt. A
318 // reload (the file changed or was deleted while running) only reads.
319 void LoadPresets(bool atStartup = false);
320 void MaybeReloadPresets();
321
322 Json::Value ReplaceCommandKeywords(Json::Value cmd, std::map<std::string, std::string>& keywords);
323 // Shared by getDescriptions() and the single-command GET route so both
324 // stay in sync on which fields (e.g. category/level) get merged in.
325 Json::Value describeCommand(Command* cmd);
326
327 std::mutex presetsMutex;
328 Json::Value presets;
329 uint64_t lastPresetTimeStamp = 0;
330
331 struct CommandMeta {
332 std::string category;
333 int level = 0;
334 };
335
336 std::map<std::string, Command*> commands;
337 std::map<std::string, CommandMeta> commandMeta;
338 std::set<std::string> missingPresets;
339
340 // Bumped on every registration change so /api/commands can answer a
341 // conditional request without re-describing every command. Sound as a
342 // version only because a command's description is fixed at construction:
343 // args are built in constructors and every list that varies at runtime is
344 // declared as a contentListUrl the client fetches separately, so nothing
345 // else can change that response. Appended here (private, and the class is
346 // a singleton plugins only ever reach through INSTANCE) so no plugin needs
347 // rebuilding.
348 std::atomic<uint64_t> commandsGeneration{0};
349
350 // Guards `commands` and `commandMeta`. Plugin load/unload and the channel
351 // output reload register and remove commands on the main loop while HTTP,
352 // MQTT and playlist threads look them up. Appended, like
353 // commandsGeneration, so the layout plugins see does not move.
354 mutable std::shared_mutex commandsLock;
355 // Runs in progress per command. removeCommand() waits for its command to
356 // go idle, because every caller deletes the command right after it.
357 std::mutex inFlightLock;
358 std::condition_variable inFlightCV;
359 std::map<Command*, int> inFlight;
360 // Retired commands still running; the last pin's release deletes each.
361 // Guarded by inFlightLock.
362 std::set<Command*> retired;
363
364 std::shared_ptr<Command> acquireCommand(const std::string& name);
365 void waitForIdle(Command* cmd);
366 };
367