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