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