GCC Code Coverage Report


Directory: ./
File: Plugins.cpp
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 772 849 90.9%
Functions: 78 83 94.0%
Branches: 910 1698 53.6%

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
17 #include <sys/stat.h>
18 #include <sys/wait.h>
19 #include <poll.h>
20 #include <signal.h>
21 #include <cstring>
22 #include <dirent.h>
23 #include <dlfcn.h>
24 #include <errno.h>
25 #include <algorithm>
26 #include <chrono>
27 #include <thread>
28 #include <list>
29 #include <map>
30 #include <memory>
31 #include <condition_variable>
32 #include <mutex>
33 #include <optional>
34 #include <set>
35 #include <stdio.h>
36 #include <stdlib.h>
37 #include <string>
38 #include <unistd.h>
39 #include <vector>
40
41 #include "FileMonitor.h"
42 #include "Plugin.h"
43 #include "Warnings.h"
44 #include "common.h"
45 #include "config.h"
46 #include "log.h"
47 #include "mediadetails.h"
48 #include "settings.h"
49 #include "commands/Commands.h"
50
51 #include "CurlManager.h"
52 #include "EPollManager.h"
53 #include "Timers.h"
54 #include "Plugins.h"
55
56 PluginManager PluginManager::INSTANCE;
57
58 // mChannelDataPlugins is the one plugin registry walked off the main loop: the
59 // channel output thread calls modifySequenceData()/modifyChannelData() every
60 // frame, while a runtime load or unload edits the list on the main loop and an
61 // unload then deletes the plugin. The output thread holds this across its whole
62 // walk, so once detachPlugin() has taken it and dropped a plugin, no frame is
63 // still inside that plugin's code and it is safe to destroy. File-static rather
64 // than a member so PluginManager's layout is unchanged for plugins.
65 static std::mutex channelDataPluginsLock;
66
67 // mPlaylistPlugins is walked from whichever thread made the playlist or media
68 // transition - the main loop, an HTTP or MQTT thread, a scheduler command -
69 // while a runtime load or unload edits it on the main loop and an unload then
70 // deletes the plugin. A walk copies the list under this lock, then, just
71 // before each call, checks under it again that the plugin is still listed and
72 // counts it as in flight for that one call; finishUnload() waits for a
73 // plugin's count to drain before deleting it. Checking per call matters: an
74 // unload that lands mid-walk must keep the plugins later in the copy from
75 // being entered after their shutdown(), and must wait only on a call that is
76 // really running, not on a slow callback into some other plugin. Nothing is
77 // held across the calls themselves: script plugins block in waitpid() for
78 // seconds, and a callback can start another transition, which walks the list
79 // again on the same thread. File-static, like channelDataPluginsLock, so
80 // PluginManager's layout is unchanged for plugins.
81 static std::mutex playlistPluginsLock;
82 static std::condition_variable playlistPluginCallDone;
83 static std::map<FPPPlugins::Plugin*, int> playlistPluginCallsInFlight;
84
85 // Plugins whose settings-file watch detachPlugin() has already removed. The
86 // watch is registered under the plugin's name, and a runtime reload constructs
87 // the new instance -- registering its watch under that same name -- while the
88 // old one is still waiting to be deleted. The old one's destructor removing the
89 // watch again would take the new instance's with it, and the reloaded plugin
90 // would never hear of a settings change until fppd restarted. File-static, like
91 // the locks above, so Plugin's layout is unchanged for plugins.
92 static std::mutex settingsUnwatchedLock;
93 static std::set<const FPPPlugins::Plugin*> settingsUnwatched;
94
95 template<class F>
96 85712 static void ForEachPlaylistPlugin(const std::vector<FPPPlugins::PlaylistEventPlugin*>& plugins, F&& f) {
97 85712 std::vector<FPPPlugins::PlaylistEventPlugin*> snapshot;
98 {
99
1/2
✓ Branch 0 taken 42856 times.
✗ Branch 1 not taken.
85712 std::lock_guard<std::mutex> lock(playlistPluginsLock);
100
1/2
✓ Branch 0 taken 42856 times.
✗ Branch 1 not taken.
85712 snapshot = plugins;
101 85712 }
102
2/2
✓ Branch 0 taken 131 times.
✓ Branch 1 taken 42854 times.
86220 for (auto a : snapshot) {
103 262 FPPPlugins::Plugin* key = nullptr;
104 {
105
1/2
✓ Branch 0 taken 131 times.
✗ Branch 1 not taken.
262 std::lock_guard<std::mutex> lock(playlistPluginsLock);
106
3/4
✓ Branch 0 taken 131 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✓ Branch 3 taken 127 times.
262 if (std::find(plugins.begin(), plugins.end(), a) == plugins.end()) {
107 8 continue; // unloaded since the copy was taken, maybe freed
108 }
109 // Only now: the cast reads the object, which is live only while
110 // it is still listed.
111
1/2
✓ Branch 0 taken 127 times.
✗ Branch 1 not taken.
254 key = dynamic_cast<FPPPlugins::Plugin*>(a);
112
1/2
✓ Branch 0 taken 127 times.
✗ Branch 1 not taken.
254 playlistPluginCallsInFlight[key]++;
113
2/2
✓ Branch 0 taken 127 times.
✓ Branch 1 taken 4 times.
262 }
114 struct Done {
115 FPPPlugins::Plugin* key;
116 125 ~Done() {
117 125 std::lock_guard<std::mutex> lock(playlistPluginsLock);
118 125 auto it = playlistPluginCallsInFlight.find(key);
119 125 if (it != playlistPluginCallsInFlight.end() && --it->second <= 0) {
120 71 playlistPluginCallsInFlight.erase(it);
121 71 playlistPluginCallDone.notify_all();
122 }
123 125 }
124 254 } done{ key };
125
1/2
✓ Branch 0 taken 125 times.
✗ Branch 1 not taken.
254 f(a);
126 }
127 85708 }
128
129 namespace FPPPlugins
130 {
131 1393 static std::map<std::string, std::string> parseSettingsFile(const std::string& dname) {
132 1393 std::map<std::string, std::string> settings;
133
3/4
✓ Branch 0 taken 1393 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 1387 times.
1393 if (FileExists(dname)) {
134
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 FILE* file = fopen(dname.c_str(), "r");
135
136
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 if (file != NULL) {
137 6 char* line = NULL;
138 6 size_t len = 0;
139 ssize_t read;
140 6 int sIndex = 0;
141
3/4
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✓ Branch 3 taken 6 times.
14 while ((read = getline(&line, &len, file)) != -1) {
142
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 ((!line) || (!read) || (read == 1)) {
143 ✗ continue;
144 }
145
146 // Split on the first '=' only and drop exactly one pair of
147 // surrounding quotes, as LoadSettings() reads FPP's own
148 // settings file: splitting on every '=' cut values such as
149 // base64 secrets short, and every quote was trimmed.
150
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 std::string l(line, read);
151 8 size_t eq = l.find('=');
152
2/4
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
8 char* key = trimwhitespace(l.substr(0, eq).c_str());
153
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (!strlen(key)) {
154 ✗ free(key);
155 ✗ continue;
156 }
157
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
8 if (eq == std::string::npos) {
158 ✗ fprintf(stderr, "Error tokenizing value for %s setting\n", key);
159 ✗ free(key);
160 ✗ continue;
161 }
162
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 std::string value = l.substr(eq + 1);
163
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 TrimWhiteSpace(value);
164
4/8
✓ 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.
8 if (value.size() >= 2 && value.front() == '"' && value.back() == '"') {
165
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 value = value.substr(1, value.size() - 2);
166 }
167
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 settings[key] = value;
168 8 free(key);
169
1/2
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
8 }
170
171
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 if (line) {
172 6 free(line);
173 }
174
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 fclose(file);
175 }
176 }
177 1393 return settings;
178 ✗ }
179
180 1389 Plugin::Plugin(const std::string& n) :
181 1389 name(n) {
182
4/8
✓ Branch 0 taken 1389 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1389 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1389 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1389 times.
✗ Branch 7 not taken.
2625 std::string dname = FPP_DIR_CONFIG("/plugin." + name);
183
1/2
✓ Branch 0 taken 1389 times.
✗ Branch 1 not taken.
1389 settings = parseSettingsFile(dname);
184 1389 }
185 2 Plugin::Plugin(const std::string& n, bool monitorSettings) :
186 2 name(n) {
187
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 if (monitorSettings) {
188
4/8
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 2 times.
✗ Branch 7 not taken.
4 std::string dname = FPP_DIR_CONFIG("/plugin." + name);
189
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 settings = parseSettingsFile(dname);
190
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 FileMonitor::INSTANCE.AddFile(name, dname, [this, dname]() {
191
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 std::map<std::string, std::string> newSettings = parseSettingsFile(dname);
192
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
4 for (auto& s : newSettings) {
193
6/10
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 1 times.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 2 times.
✗ Branch 9 not taken.
2 if (settings.find(s.first) == settings.end() || settings[s.first] != s.second) {
194
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogInfo(VB_PLUGIN, "Plugin %s setting %s changed to %s\n", name.c_str(), s.first.c_str(), s.second.c_str());
195
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 settings[s.first] = s.second;
196
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 settingChanged(s.first, s.second);
197 }
198 }
199 2 });
200 2 }
201 2 }
202
203 2778 Plugin::~Plugin() {
204 {
205 2778 std::lock_guard<std::mutex> lock(settingsUnwatchedLock);
206
2/2
✓ Branch 0 taken 40 times.
✓ Branch 1 taken 1349 times.
2778 if (settingsUnwatched.erase(this)) {
207 80 return;
208 }
209
2/2
✓ Branch 0 taken 1349 times.
✓ Branch 1 taken 40 times.
2778 }
210 5396 FileMonitor::INSTANCE.RemoveFile(name, FPP_DIR_CONFIG("/plugin." + name));
211
4/4
✓ Branch 0 taken 1349 times.
✓ Branch 1 taken 40 times.
✓ Branch 2 taken 1349 times.
✓ Branch 3 taken 40 times.
2858 }
212
213 ✗ void Plugin::reloadSettings() {
214 ✗ std::string dname = FPP_DIR_CONFIG("/plugin." + name);
215 ✗ settings = parseSettingsFile(dname);
216 ✗ }
217 }
218 class LifecycleCallback {
219 public:
220 43 LifecycleCallback(const std::string& name, const std::string& filename) :
221 43 mName(name),
222
1/2
✓ Branch 0 taken 43 times.
✗ Branch 1 not taken.
43 mFilename(filename) {
223 43 }
224 168 virtual ~LifecycleCallback() {}
225
226 void run(const std::string& lifecycle);
227
228 private:
229 std::string mName;
230 std::string mFilename;
231 };
232
233 class MediaCallback {
234 public:
235 2 MediaCallback(const std::string& name, const std::string& filename) :
236 2 mName(name),
237
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 mFilename(filename) {
238 2 }
239 8 virtual ~MediaCallback() {}
240
241 void run(const Json::Value& playlist, const MediaDetails& mediaDetails);
242
243 private:
244 std::string mName;
245 std::string mFilename;
246 };
247
248 class PlaylistCallback {
249 public:
250 47 PlaylistCallback(const std::string& name, const std::string& filename) :
251 47 mName(name),
252
1/2
✓ Branch 0 taken 47 times.
✗ Branch 1 not taken.
47 mFilename(filename) {
253 47 }
254 180 virtual ~PlaylistCallback() {}
255
256 void run(const Json::Value& playlist, const std::string& action, const std::string& section, int idx);
257
258 private:
259 std::string mName;
260 std::string mFilename;
261 };
262
263 extern MediaDetails mediaDetails;
264
265 const char* type_to_string[] = {
266 "both",
267 "media",
268 "sequence",
269 "pause",
270 "video",
271 };
272
273 class ScriptFPPPlugin : public FPPPlugins::Plugin, public FPPPlugins::PlaylistEventPlugin {
274 public:
275 128 ScriptFPPPlugin(const std::string& n, const std::string& filename, const std::string& lst) :
276
1/2
✓ Branch 0 taken 128 times.
✗ Branch 1 not taken.
128 FPPPlugins::Plugin(n), FPPPlugins::PlaylistEventPlugin(), fileName(filename) {
277
1/2
✓ Branch 0 taken 128 times.
✗ Branch 1 not taken.
128 std::vector<std::string> types = split(lst, ',');
278
2/2
✓ Branch 0 taken 170 times.
✓ Branch 1 taken 128 times.
298 for (int i = 0; i < types.size(); i++) {
279
3/4
✓ Branch 0 taken 170 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 168 times.
170 if (types[i] == "media") {
280
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogDebug(VB_PLUGIN, "Plugin %s supports media callback.\n", name.c_str());
281
3/8
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 2 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
2 m_mediaCallback = new MediaCallback(name, filename);
282
3/4
✓ Branch 0 taken 168 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 47 times.
✓ Branch 3 taken 121 times.
168 } else if (types[i] == "playlist") {
283
1/2
✓ Branch 0 taken 47 times.
✗ Branch 1 not taken.
47 LogDebug(VB_PLUGIN, "Plugin %s supports playlist callback.\n", name.c_str());
284
3/8
✓ Branch 0 taken 47 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 47 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 47 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
47 m_playlistCallback = new PlaylistCallback(name, filename);
285
3/4
✓ Branch 0 taken 121 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 43 times.
✓ Branch 3 taken 78 times.
121 } else if (types[i] == "lifecycle") {
286
3/8
✓ Branch 0 taken 43 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 43 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 43 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
43 m_lifecycleCallback = new LifecycleCallback(name, filename);
287 } else {
288
1/2
✓ Branch 0 taken 78 times.
✗ Branch 1 not taken.
78 otherTypes.push_back(types[i]);
289 }
290 }
291
2/4
✓ Branch 0 taken 128 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 128 times.
✗ Branch 3 not taken.
128 lifecycleCallback("startup");
292 128 }
293 504 virtual ~ScriptFPPPlugin() {
294 252 lifecycleCallback("shutdown");
295
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 124 times.
252 if (m_mediaCallback)
296
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
4 delete m_mediaCallback;
297
2/2
✓ Branch 0 taken 45 times.
✓ Branch 1 taken 81 times.
252 if (m_playlistCallback)
298
1/2
✓ Branch 0 taken 45 times.
✗ Branch 1 not taken.
90 delete m_playlistCallback;
299
2/2
✓ Branch 0 taken 42 times.
✓ Branch 1 taken 84 times.
252 if (m_lifecycleCallback)
300
1/2
✓ Branch 0 taken 42 times.
✗ Branch 1 not taken.
84 delete m_lifecycleCallback;
301 504 }
302
303 128 bool hasCallback() const {
304
6/6
✓ Branch 0 taken 126 times.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 80 times.
✓ Branch 3 taken 46 times.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 78 times.
128 return m_mediaCallback != nullptr || m_playlistCallback != nullptr || m_lifecycleCallback != nullptr;
305 }
306
307 78 const std::list<std::string>& getOtherTypes() const {
308 78 return otherTypes;
309 }
310
311 4 virtual void mediaCallback(const Json::Value& playlist, const MediaDetails& mediaDetails) override {
312
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 if (m_mediaCallback) {
313 4 m_mediaCallback->run(playlist, mediaDetails);
314 }
315 4 }
316 123 virtual void playlistCallback(const Json::Value& playlist, const std::string& action, const std::string& section, int item) override {
317
2/2
✓ Branch 0 taken 112 times.
✓ Branch 1 taken 11 times.
123 if (m_playlistCallback) {
318 112 m_playlistCallback->run(playlist, action, section, item);
319 }
320 121 }
321 254 virtual void lifecycleCallback(const std::string& lifecycle) {
322
2/2
✓ Branch 0 taken 85 times.
✓ Branch 1 taken 169 times.
254 if (m_lifecycleCallback) {
323 85 m_lifecycleCallback->run(lifecycle);
324 }
325 254 }
326
327 private:
328 const std::string fileName;
329
330 std::list<std::string> otherTypes;
331 MediaCallback* m_mediaCallback = nullptr;
332 PlaylistCallback* m_playlistCallback = nullptr;
333 LifecycleCallback* m_lifecycleCallback = nullptr;
334 };
335
336 2346 PluginManager::PluginManager() {
337 2346 }
338
339 class ScriptCommand : public Command {
340 public:
341 6 ScriptCommand(const std::string& dir, Json::Value& json) :
342
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
12 Command(json["name"].asString()),
343 6 directory(dir),
344
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.
18 description(json) {
345
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
6 script = json["script"].asString();
346
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 description.removeMember("script");
347 6 }
348 6 bool IsOk() {
349
3/6
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 6 times.
✗ Branch 5 not taken.
6 return FileExists(directory + "/" + script);
350 }
351 2 virtual Json::Value getDescription() override {
352 2 return description;
353 }
354
355 5 virtual std::unique_ptr<Result> run(const std::vector<std::string>& args) override {
356 // Everything the child needs is built before the fork: fppd is
357 // threaded, and a child that allocates or logs can block forever on a
358 // lock another thread held at the fork (see PlaylistCallback::run()).
359
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 std::string eventScript = directory + "/" + script;
360
3/6
✓ 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.
10 std::string mediaDir = "MEDIADIR=" + getFPPMediaDir();
361
3/6
✓ 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.
5 std::string fppDir = "FPPDIR=" + getFPPDDir();
362
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 std::string pluginDir = "SCRIPTDIR=" + directory;
363
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
10 std::vector<const char*> envs = { mediaDir.c_str(), fppDir.c_str(), pluginDir.c_str(), nullptr };
364 5 std::vector<const char*> sargs;
365
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 sargs.push_back(eventScript.c_str());
366
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 5 times.
7 for (auto& a : args) {
367
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 sargs.push_back(a.c_str());
368 }
369
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 sargs.push_back(nullptr);
370
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 LogDebug(VB_PLUGIN, "Calling %s for command: %s\n", script.c_str(), name.c_str());
371
372 5 int pid = fork();
373
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 8 times.
9 if (pid == -1) {
374 // Usually out of memory. This runs on whatever thread ran the
375 // command, so exiting took all of fppd down for one plugin call.
376
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 LogErr(VB_PLUGIN, "Failed to fork for plugin command %s: %s\n", name.c_str(), FPPstrerror(errno));
377
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 return std::make_unique<Command::ErrorResult>(name + " failed: could not start the script");
378 }
379
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 4 times.
8 if (pid == 0) {
380 4 execve(eventScript.c_str(), (char* const*)&sargs[0], (char* const*)&envs[0]);
381 4 _exit(EXIT_FAILURE);
382 }
383
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 LogExcess(VB_PLUGIN, "Command parent process, resuming work.\n");
384
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 waitpid(pid, nullptr, 0);
385
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 return std::make_unique<Command::Result>(name + " complete");
386 5 }
387
388 std::string directory;
389 std::string script;
390 Json::Value description;
391 };
392
393 namespace {
394 // Attributes to a plugin every command that appears in the registry while this
395 // object is alive. See mPluginCommands in Plugins.h for why ownership is
396 // deduced this way rather than reported. Scoped rather than open-coded because
397 // the load path it wraps has several early returns.
398 class CommandRecorder {
399 public:
400 145 CommandRecorder(std::map<std::string, std::vector<std::pair<std::string, Command*>>>& into,
401 145 const std::string& plugin) :
402 145 mInto(into),
403 145 mPlugin(plugin) {
404
3/4
✓ Branch 0 taken 145 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8745 times.
✓ Branch 3 taken 145 times.
8890 for (const auto& c : CommandManager::INSTANCE.getRegisteredCommands()) {
405
1/2
✓ Branch 0 taken 8745 times.
✗ Branch 1 not taken.
8745 mBefore.insert(c.first);
406 145 }
407 145 }
408 145 ~CommandRecorder() {
409 145 std::vector<std::pair<std::string, Command*>> added;
410
2/2
✓ Branch 0 taken 8825 times.
✓ Branch 1 taken 145 times.
8970 for (const auto& c : CommandManager::INSTANCE.getRegisteredCommands()) {
411
2/2
✓ Branch 0 taken 80 times.
✓ Branch 1 taken 8745 times.
8825 if (mBefore.find(c.first) == mBefore.end()) {
412 80 added.push_back(c);
413 }
414 145 }
415
2/2
✓ Branch 0 taken 51 times.
✓ Branch 1 taken 94 times.
145 if (!added.empty()) {
416 51 auto& slot = mInto[mPlugin];
417 51 slot.insert(slot.end(), added.begin(), added.end());
418 51 LogDebug(VB_PLUGIN, "Plugin %s registered %d command(s)\n", mPlugin.c_str(), (int)added.size());
419 }
420 145 }
421 CommandRecorder(const CommandRecorder&) = delete;
422 CommandRecorder& operator=(const CommandRecorder&) = delete;
423
424 private:
425 std::map<std::string, std::vector<std::pair<std::string, Command*>>>& mInto;
426 std::string mPlugin;
427 std::set<std::string> mBefore;
428 };
429 } // namespace
430
431 // Registers the commands a plugin declares in commands/descriptions.json. These
432 // are FPP-owned objects describing scripts in the plugin's directory, and an
433 // uninstalled plugin should not leave its commands listed - but withdrawing
434 // them again is not this function's job. They go into the registry through
435 // addCommand() like any other, so the CommandRecorder wrapping the load
436 // attributes them to the plugin and unloadPlugin() takes them back from there.
437 130 static void LoadPluginCommands(const std::string& dir) {
438
5/10
✓ Branch 0 taken 130 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 130 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 130 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 130 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 130 times.
✗ Branch 9 not taken.
130 std::string commandDir = FPP_DIR_PLUGIN("/" + dir + "/commands/");
439
1/2
✓ Branch 0 taken 130 times.
✗ Branch 1 not taken.
130 std::string descriptions = commandDir + "/descriptions.json";
440
3/4
✓ Branch 0 taken 130 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✓ Branch 3 taken 124 times.
130 if (FileExists(descriptions)) {
441
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 Json::Value json = LoadJsonFromFile(descriptions, JsonRoot::Array);
442
3/4
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✓ Branch 3 taken 6 times.
19 for (Json::ArrayIndex x = 0; x < json.size(); x++) {
443
2/4
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
13 Json::Value jscmd = json[x];
444 // Read at fppd startup: an entry ScriptCommand can't read (not an
445 // object, or a name or script that isn't a string) is skipped, not
446 // thrown on -- one plugin's typo would otherwise stop fppd starting.
447
14/22
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 10 times.
✓ Branch 3 taken 3 times.
✓ Branch 4 taken 10 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 10 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 8 times.
✓ Branch 9 taken 2 times.
✓ Branch 10 taken 8 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 8 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 8 times.
✗ Branch 15 not taken.
✓ Branch 16 taken 8 times.
✓ Branch 17 taken 5 times.
✓ Branch 18 taken 7 times.
✓ Branch 19 taken 6 times.
✗ Branch 20 not taken.
✗ Branch 21 not taken.
21 if (!jscmd.isObject() || !jscmd["name"].isString() || jscmd["name"].asString().empty() ||
448
4/6
✓ Branch 0 taken 8 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 8 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2 times.
✓ Branch 5 taken 6 times.
8 !jscmd["script"].isString()) {
449
3/6
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 7 times.
✗ Branch 5 not taken.
7 WarningHolder::InvalidConfigEntry("plugins/" + dir + "/commands/descriptions.json",
450
2/4
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
14 "command " + std::to_string(x + 1) + " needs a name and a script");
451 7 continue;
452 }
453
3/8
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 6 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
6 ScriptCommand* cmd = new ScriptCommand(commandDir, jscmd);
454
2/4
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
6 if (cmd->IsOk()) {
455
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 CommandManager::INSTANCE.addCommand(cmd);
456 } else {
457 ✗ delete cmd;
458 }
459
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 7 times.
13 }
460 6 }
461 130 }
462
463 1887 void PluginManager::init() {
464 1887 }
465 1887 void PluginManager::loadUserPlugins() {
466 DIR* dp;
467 struct dirent* ep;
468
469
4/8
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1887 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1887 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1887 times.
✗ Branch 7 not taken.
1887 dp = opendir(FPP_DIR_PLUGIN("").c_str());
470
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 if (dp != NULL) {
471
2/2
✓ Branch 0 taken 3869 times.
✓ Branch 1 taken 1887 times.
5756 while ((ep = readdir(dp))) {
472 // Skip ".", "..", and hidden entries. The test is on the LEADING
473 // character: a plugin directory is normally named "fpp-Something"
474 // and contains no dot at all, so keying off "has a dot anywhere"
475 // skips every real plugin instead of the three entries meant to be
476 // excluded.
477
2/2
✓ Branch 0 taken 3774 times.
✓ Branch 1 taken 95 times.
3869 if (ep->d_name[0] == '.') {
478 3777 continue;
479 }
480 struct stat statbuf;
481
4/8
✓ Branch 0 taken 95 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 95 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 95 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 95 times.
✗ Branch 7 not taken.
188 std::string dname = FPP_DIR_PLUGIN("/" + ep->d_name);
482 95 lstat(dname.c_str(), &statbuf);
483
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 92 times.
95 if (!S_ISDIR(statbuf.st_mode)) {
484
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 dname += "/.linkOK"; // Allow developers to use symlinks if desired
485
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 if (!FileExists(dname))
486 3 continue;
487 }
488
4/6
✓ Branch 0 taken 92 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 92 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 82 times.
✓ Branch 5 taken 10 times.
276 if (mLoadedUserPlugins.find(ep->d_name) == mLoadedUserPlugins.end()) {
489
2/4
✓ Branch 0 taken 82 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 82 times.
✗ Branch 3 not taken.
166 loadUserPlugin(ep->d_name);
490 }
491
2/2
✓ Branch 0 taken 92 times.
✓ Branch 1 taken 3 times.
95 }
492 1887 closedir(dp);
493 } else {
494 ✗ LogWarn(VB_PLUGIN, "Couldn't open the directory %s: (%d): %s\n", FPP_DIR_PLUGIN("").c_str(), errno, FPPstrerror(errno));
495 }
496
497 // After an FPPOS reflash the boot code sets pluginReinstallNeededAfterOS to
498 // "1" if plugins were present. The Plugin Manager turns that into the list
499 // of plugins still to reinstall and clears it once every one has been
500 // reinstalled or uninstalled. Surface the reinstall prompt, and keep it in
501 // sync while fppd is running. Native plugins still on the list were not
502 // loaded above - see loadUserPlugin().
503 static bool reinstallListenerRegistered = false;
504
1/2
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
1887 if (!reinstallListenerRegistered) {
505 1887 reinstallListenerRegistered = true;
506
3/6
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1887 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1887 times.
✗ Branch 5 not taken.
7708 registerSettingsListener("PluginManager", "pluginReinstallNeededAfterOS",
507 3774 [this](const std::string&) { checkPluginReinstallWarning(); });
508 }
509 1887 checkPluginReinstallWarning();
510
511 1887 return;
512 }
513
514 // Warning id shared with the www/warnings-definitions.json "Plugin_Warnings"
515 // entry (drives the icon/grouping); the Fix button itself is rendered from the
516 // fixUrl/fixText carried in the warning data.
517 static constexpr int PLUGIN_REINSTALL_WARNING_ID = 57;
518 // Not only after an FPPOS upgrade: a File Copy restore of plugins sets it too.
519 static const std::string PLUGIN_REINSTALL_WARNING_MSG = "Plugins must be reinstalled";
520
521 1887 void PluginManager::checkPluginReinstallWarning() {
522
4/6
✓ Branch 0 taken 1887 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1887 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 1886 times.
1887 if (getSetting("pluginReinstallNeededAfterOS") != "") {
523
3/6
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
4 WarningHolder::AddWarning(PLUGIN_REINSTALL_WARNING_ID, PLUGIN_REINSTALL_WARNING_MSG,
524 "plugins.php?action=reinstallAll", "Reinstall All Plugins");
525 } else {
526
2/4
✓ Branch 0 taken 1886 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1886 times.
✗ Branch 3 not taken.
3772 WarningHolder::RemoveWarning(PLUGIN_REINSTALL_WARNING_ID, PLUGIN_REINSTALL_WARNING_MSG);
527 }
528 1887 }
529
530 458 PluginManager::~PluginManager() {
531 458 Cleanup();
532 458 }
533 2325 void PluginManager::Cleanup() {
534 // Give API-providing plugins their unregister callback before destroying
535 // them: that is where they remove (and may delete) the Command objects
536 // they added to CommandManager. Without this, those commands are still
537 // registered when CommandManager::Cleanup() bulk-deletes everything it
538 // holds, and a plugin destructor that also deletes them double-frees.
539
1/2
✓ Branch 0 taken 2325 times.
✗ Branch 1 not taken.
2325 unregisterApis();
540 2325 mAPIProviderPlugins.clear();
541 // Anything still settling from an unload will never get another timer tick
542 // now, so finish it here rather than leaking the object (and, for an opted-in
543 // plugin, the mapping) on the way out.
544
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2325 times.
2325 while (!mPendingUnloads.empty()) {
545 ✗ PendingUnload p = std::move(mPendingUnloads.back());
546 ✗ mPendingUnloads.pop_back();
547 ✗ finishUnload(p);
548 ✗ }
549 // A playlist or media callback can still be inside a plugin on another
550 // thread: the MQTT thread handling a playlist command, or a command thread.
551 // Nothing waited for those, so the deletes below could free a plugin (a
552 // script plugin blocks in waitpid() for as long as its script runs) while
553 // it was still running. Detach them all, as an unload does, and wait for
554 // the calls in flight -- briefly: shutdown has a budget. A plugin still
555 // busy past that is left alive, and so is every plugin library, since its
556 // code may still be running: each path from here ends in exec or _exit.
557 2325 std::set<FPPPlugins::Plugin*> busy;
558 {
559
1/2
✓ Branch 0 taken 2325 times.
✗ Branch 1 not taken.
2325 std::unique_lock<std::mutex> lock(playlistPluginsLock);
560 2325 mPlaylistPlugins.clear();
561
3/4
✓ Branch 0 taken 2325 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 2323 times.
2325 if (!playlistPluginCallDone.wait_for(lock, std::chrono::seconds(2), []() {
562 2329 return playlistPluginCallsInFlight.empty();
563 })) {
564
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
4 for (auto& [plugin, n] : playlistPluginCallsInFlight) {
565
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 busy.insert(plugin);
566 }
567 }
568 2325 }
569
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2325 times.
2327 for (auto* a : busy) {
570
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogWarn(VB_PLUGIN, "Plugin %s is still in a playlist callback at shutdown; leaving it\n",
571 a->getName().c_str());
572 }
573 // Inbound HTTP is disarmed above and the routes have drained, so no request
574 // is inside plugin code. Now let each plugin stop its own activity - threads,
575 // timers, connections - while it is still a whole object. Destructors are too
576 // late for that: a thread calling in during destruction reads a plugin that
577 // is half gone.
578
2/2
✓ Branch 0 taken 1273 times.
✓ Branch 1 taken 2325 times.
3598 for (auto& a : mPlugins) {
579
3/4
✓ Branch 0 taken 1273 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1271 times.
✓ Branch 3 taken 2 times.
1273 if (!busy.count(a)) {
580
1/2
✓ Branch 0 taken 1271 times.
✗ Branch 1 not taken.
1271 a->shutdown();
581 }
582 }
583
2/2
✓ Branch 0 taken 1273 times.
✓ Branch 1 taken 2325 times.
3598 while (!mPlugins.empty()) {
584
3/4
✓ Branch 0 taken 1273 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1271 times.
✓ Branch 3 taken 2 times.
1273 if (!busy.count(mPlugins.back())) {
585
1/2
✓ Branch 0 taken 1271 times.
✗ Branch 1 not taken.
1271 delete mPlugins.back();
586 }
587 1273 mPlugins.pop_back();
588 }
589 2325 mPluginDirNames.clear();
590 // Delete any commands that plugins registered but did not remove in
591 // unregisterApis(). Those commands have vtables in the plugin library.
592 // CommandManager::Cleanup() must run before dlclose() so that the virtual
593 // destructor dispatch doesn't hit unmapped memory. The call is idempotent
594 // (guarded by an atomic flag), so the subsequent call in main() is a no-op.
595
1/2
✓ Branch 0 taken 2325 times.
✗ Branch 1 not taken.
2325 CommandManager::INSTANCE.Cleanup();
596
2/2
✓ Branch 0 taken 2323 times.
✓ Branch 1 taken 2 times.
2325 if (busy.empty()) {
597
2/2
✓ Branch 0 taken 1258 times.
✓ Branch 1 taken 2323 times.
3581 for (auto& a : mShlibHandles) {
598 1258 dlclose(a);
599 }
600 }
601 2325 mShlibHandles.clear();
602 2325 }
603
604 1279 FPPPlugins::Plugin* PluginManager::findPlugin(const std::string& name, const std::string& shlibName) {
605
2/2
✓ Branch 0 taken 1030 times.
✓ Branch 1 taken 1209 times.
2239 for (auto& a : mPlugins) {
606
2/2
✓ Branch 0 taken 70 times.
✓ Branch 1 taken 960 times.
1030 if (a->getName() == name) {
607 70 return a;
608 }
609 }
610
4/8
✓ Branch 0 taken 1209 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 1209 times.
✓ Branch 4 taken 1209 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1209 times.
✗ Branch 7 not taken.
1209 std::string libName = "lib" + (shlibName == "" ? name : shlibName) + SHLIB_EXT;
611
1/2
✓ Branch 0 taken 1209 times.
✗ Branch 1 not taken.
1209 FPPPlugins::Plugin* p = loadSHLIBPlugin(libName, name);
612
2/2
✓ Branch 0 taken 13 times.
✓ Branch 1 taken 1196 times.
1209 if (!p) {
613
16/38
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 13 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 13 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 13 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 10 times.
✓ Branch 11 taken 3 times.
✓ Branch 12 taken 10 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 10 times.
✗ Branch 15 not taken.
✓ Branch 16 taken 13 times.
✗ Branch 17 not taken.
✓ Branch 18 taken 13 times.
✗ Branch 19 not taken.
✓ Branch 20 taken 13 times.
✗ Branch 21 not taken.
✓ Branch 22 taken 13 times.
✗ Branch 23 not taken.
✓ Branch 24 taken 13 times.
✗ Branch 25 not taken.
✓ Branch 26 taken 10 times.
✓ Branch 27 taken 3 times.
✗ Branch 28 not taken.
✗ Branch 29 not taken.
✗ Branch 30 not taken.
✗ Branch 31 not taken.
✗ Branch 32 not taken.
✗ Branch 33 not taken.
✗ Branch 34 not taken.
✗ Branch 35 not taken.
✗ Branch 36 not taken.
✗ Branch 37 not taken.
39 if (DirectoryExists(FPP_DIR_PLUGIN("/" + name)) && (mLoadedUserPlugins.find(name) == mLoadedUserPlugins.end())) {
614
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 loadUserPlugin(name);
615 }
616
2/2
✓ Branch 0 taken 13 times.
✓ Branch 1 taken 3 times.
16 for (auto& a : mPlugins) {
617
2/2
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 3 times.
13 if (a->getName() == name) {
618 10 return a;
619 }
620 }
621 }
622
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1196 times.
1199 if (!p) {
623
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 LogErr(VB_PLUGIN, "Failed to load plugin: %s Error: %s\n", libName.c_str(), dlerror());
624 }
625 1199 return p;
626 1209 }
627 68 bool PluginManager::awaitingReinstallAfterOS(const std::string& dirName) {
628
1/2
✓ Branch 0 taken 68 times.
✗ Branch 1 not taken.
68 std::string v = getSetting("pluginReinstallNeededAfterOS");
629
1/2
✓ Branch 0 taken 68 times.
✗ Branch 1 not taken.
68 TrimWhiteSpace(v);
630
2/2
✓ Branch 0 taken 67 times.
✓ Branch 1 taken 1 times.
68 if (v.empty()) {
631 67 return false;
632 }
633 // Boot writes "1" (every installed plugin); the Plugin Manager turns that
634 // into the comma-separated list of plugins still to reinstall.
635
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 if (v == "1") {
636 1 return true;
637 }
638 ✗ for (auto& p : split(v, ',')) {
639 ✗ TrimWhiteSpace(p);
640 ✗ if (p == dirName) {
641 ✗ return true;
642 }
643 ✗ }
644 ✗ return false;
645 68 }
646
647 130 FPPPlugins::Plugin* PluginManager::loadUserPlugin(const std::string& name, bool explicitLoad) {
648
1/2
✓ Branch 0 taken 130 times.
✗ Branch 1 not taken.
130 LogDebug(VB_PLUGIN, "Found Plugin: (%s)\n", name.c_str());
649 // Covers the whole load, so it catches both the commands FPP registers from
650 // the plugin's descriptions.json below and the ones a C++ plugin registers
651 // from its own constructor, which runs inside loadSHLIBPlugin().
652
1/2
✓ Branch 0 taken 130 times.
✗ Branch 1 not taken.
130 CommandRecorder recordCommands(mPluginCommands, name);
653
5/10
✓ Branch 0 taken 130 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 130 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 130 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 130 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 130 times.
✗ Branch 9 not taken.
130 std::string filename = FPP_DIR_PLUGIN("/" + name + "/callbacks");
654 130 bool found = false;
655
656
3/4
✓ Branch 0 taken 130 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 14 times.
✓ Branch 3 taken 116 times.
130 if (FileExists(filename)) {
657
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
14 printf("Found callback with no extension");
658 14 found = true;
659 } else {
660 116 std::vector<std::string> extensions;
661
2/4
✓ Branch 0 taken 116 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 116 times.
✗ Branch 3 not taken.
232 extensions.push_back(std::string(".sh"));
662
2/4
✓ Branch 0 taken 116 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 116 times.
✗ Branch 3 not taken.
232 extensions.push_back(std::string(".pl"));
663
2/4
✓ Branch 0 taken 116 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 116 times.
✗ Branch 3 not taken.
232 extensions.push_back(std::string(".php"));
664
2/4
✓ Branch 0 taken 116 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 116 times.
✗ Branch 3 not taken.
116 extensions.push_back(std::string(".py"));
665
666
2/2
✓ Branch 0 taken 464 times.
✓ Branch 1 taken 116 times.
580 for (std::vector<std::string>::iterator i = extensions.begin(); i != extensions.end(); ++i) {
667
1/2
✓ Branch 0 taken 464 times.
✗ Branch 1 not taken.
464 std::string tmpFilename = filename + *i;
668
3/4
✓ Branch 0 taken 464 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 115 times.
✓ Branch 3 taken 349 times.
464 if (FileExists(tmpFilename.c_str())) {
669
1/2
✓ Branch 0 taken 115 times.
✗ Branch 1 not taken.
115 filename += *i;
670 115 found = true;
671 }
672 464 }
673 116 }
674 // Registered with CommandManager, so the CommandRecorder above attributes
675 // these to the plugin along with everything else it adds.
676
1/2
✓ Branch 0 taken 130 times.
✗ Branch 1 not taken.
130 LoadPluginCommands(name);
677
678
3/6
✓ Branch 0 taken 130 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 130 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 130 times.
✗ Branch 5 not taken.
130 std::string eventScript = FPP_DIR + "/scripts/eventScript";
679
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 129 times.
130 if (!found) {
680
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogExcess(VB_PLUGIN, "No callbacks supported by plugin: '%s'\n", name.c_str());
681 1 return nullptr;
682 }
683
1/2
✓ Branch 0 taken 129 times.
✗ Branch 1 not taken.
129 LogDebug(VB_PLUGIN, "Processing Callbacks (%s) for plugin: '%s'\n", filename.c_str(), name.c_str());
684
685 int output_pipe[2], pid, bytes_read;
686 char readbuffer[128];
687
1/2
✓ Branch 0 taken 129 times.
✗ Branch 1 not taken.
129 std::string callback_list = "";
688
689 // A failure here (out of memory or fds) skips this one plugin rather
690 // than exiting fppd over it.
691
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 129 times.
129 if (pipe(output_pipe) == -1) {
692 ✗ LogErr(VB_PLUGIN, "Plugin %s: failed to make a pipe for %s --list: %s; not loading it\n",
693 name.c_str(), filename.c_str(), FPPstrerror(errno));
694 ✗ return nullptr;
695 }
696
697
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 258 times.
129 if ((pid = fork()) == -1) {
698 ✗ LogErr(VB_PLUGIN, "Plugin %s: failed to fork for %s --list: %s; not loading it\n",
699 name.c_str(), filename.c_str(), FPPstrerror(errno));
700 ✗ close(output_pipe[0]);
701 ✗ close(output_pipe[1]);
702 ✗ return nullptr;
703 }
704
705
2/2
✓ Branch 0 taken 129 times.
✓ Branch 1 taken 129 times.
258 if (pid == 0) {
706 // Its own process group, so a timeout below can end the script and
707 // anything it started.
708 129 setpgid(0, 0);
709 129 dup2(output_pipe[1], STDOUT_FILENO);
710
1/2
✓ Branch 0 taken 129 times.
✗ Branch 1 not taken.
129 close(output_pipe[1]);
711
1/2
✓ Branch 0 taken 129 times.
✗ Branch 1 not taken.
129 close(output_pipe[0]);
712 129 execl(eventScript.c_str(), "eventScript", filename.c_str(), "--list", NULL);
713 129 _exit(EXIT_FAILURE);
714 } else {
715
1/2
✓ Branch 0 taken 129 times.
✗ Branch 1 not taken.
129 close(output_pipe[1]);
716
717 // This runs at startup, so a plugin whose script never answers (stuck
718 // on a network, say) would keep fppd from ever starting. Give it a
719 // while, then give up on the plugin.
720 129 constexpr int LIST_TIMEOUT_MS = 10000;
721
1/2
✓ Branch 0 taken 129 times.
✗ Branch 1 not taken.
129 long long deadline = GetMonotonicTimeMS() + LIST_TIMEOUT_MS;
722 129 bool timedOut = false;
723 while (true) {
724
1/2
✓ Branch 0 taken 257 times.
✗ Branch 1 not taken.
257 long long left = deadline - GetMonotonicTimeMS();
725 257 struct pollfd pfd = { output_pipe[0], POLLIN, 0 };
726
2/4
✓ Branch 0 taken 257 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 257 times.
✗ Branch 3 not taken.
257 int pr = left > 0 ? poll(&pfd, 1, (int)left) : 0;
727
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 257 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
257 if (pr < 0 && errno == EINTR) {
728 ✗ continue;
729 }
730
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 256 times.
257 if (pr <= 0) {
731 1 timedOut = (pr == 0);
732 1 break;
733 }
734
1/2
✓ Branch 0 taken 256 times.
✗ Branch 1 not taken.
256 bytes_read = read(output_pipe[0], readbuffer, sizeof(readbuffer) - 1);
735
736
2/2
✓ Branch 0 taken 128 times.
✓ Branch 1 taken 128 times.
256 if (bytes_read <= 0)
737 128 break;
738
739 128 readbuffer[bytes_read] = '\0';
740
1/2
✓ Branch 0 taken 128 times.
✗ Branch 1 not taken.
128 callback_list += readbuffer;
741 128 }
742
1/2
✓ Branch 0 taken 129 times.
✗ Branch 1 not taken.
129 close(output_pipe[0]);
743
744
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 128 times.
129 if (timedOut) {
745 1 kill(-pid, SIGKILL);
746
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 waitpid(pid, NULL, 0);
747
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogErr(VB_PLUGIN, "Plugin %s: %s --list did not answer within %d seconds; not loading it\n",
748 name.c_str(), filename.c_str(), LIST_TIMEOUT_MS / 1000);
749
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 WarningHolder::AddWarning(5, "Could not load plugin " + name);
750 1 return nullptr;
751 }
752
753
1/2
✓ Branch 0 taken 128 times.
✗ Branch 1 not taken.
128 TrimWhiteSpace(callback_list);
754
755
1/2
✓ Branch 0 taken 128 times.
✗ Branch 1 not taken.
128 LogExcess(VB_PLUGIN, "Callback output: (%s)\n", callback_list.c_str());
756
1/2
✓ Branch 0 taken 128 times.
✗ Branch 1 not taken.
128 waitpid(pid, NULL, 0);
757 }
758
759
3/8
✓ Branch 0 taken 128 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 128 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 128 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
128 ScriptFPPPlugin* spl = new ScriptFPPPlugin(name, filename, callback_list);
760
2/2
✓ Branch 0 taken 50 times.
✓ Branch 1 taken 78 times.
128 if (spl->hasCallback()) {
761
1/2
✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
50 mLoadedUserPlugins.emplace(name);
762
2/4
✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 50 times.
✗ Branch 3 not taken.
50 mPluginDirNames[spl] = name;
763
1/2
✓ Branch 0 taken 50 times.
✗ Branch 1 not taken.
50 addPlugin(spl);
764 50 return spl;
765 } else {
766
1/2
✓ Branch 0 taken 78 times.
✗ Branch 1 not taken.
78 for (auto& a : spl->getOtherTypes()) {
767
3/6
✓ Branch 0 taken 78 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 78 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 78 times.
✗ Branch 5 not taken.
156 if (startsWith(a, "c++")) {
768
7/14
✓ Branch 0 taken 78 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 78 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 78 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 78 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 78 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 78 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 78 times.
✗ Branch 13 not taken.
78 std::string shlibName = FPP_DIR_PLUGIN("/" + name + "/lib" + name + SHLIB_EXT);
769
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 72 times.
78 if (a[3] == ':') {
770
7/14
✓ 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.
✓ Branch 8 taken 6 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 6 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 6 times.
✗ Branch 13 not taken.
18 shlibName = FPP_DIR_PLUGIN("/" + name + "/" + a.substr(4));
771 }
772
1/2
✓ Branch 0 taken 78 times.
✗ Branch 1 not taken.
78 delete spl;
773
1/2
✓ Branch 0 taken 78 times.
✗ Branch 1 not taken.
78 mLoadedUserPlugins.emplace(name);
774 // An FPPOS reflash replaces FPP but keeps the plugin clones, so
775 // this library was built against the previous FPP's headers.
776 // The API version check cannot be relied on to refuse it - it
777 // only catches what someone remembered to bump for, and 10.2
778 // shipped layout changes without a bump - and a stale library
779 // that does load can crash fppd in its constructor (a boot
780 // loop) or in shutdown() when the Plugin Manager unloads it to
781 // reinstall. So do not load it at all until it is rebuilt; the
782 // reinstall warning is already up, and the Plugin Manager's
783 // post-rebuild load request comes through with explicitLoad.
784
7/8
✓ Branch 0 taken 68 times.
✓ Branch 1 taken 10 times.
✓ Branch 2 taken 68 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 67 times.
✓ Branch 6 taken 1 times.
✓ Branch 7 taken 77 times.
78 if (!explicitLoad && awaitingReinstallAfterOS(name)) {
785
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogWarn(VB_PLUGIN, "Not loading plugin %s: FPPOS was upgraded and it has not been reinstalled, "
786 "so %s was built against the previous FPP\n",
787 name.c_str(), shlibName.c_str());
788 1 return nullptr;
789 }
790
2/2
✓ Branch 0 taken 76 times.
✓ Branch 1 taken 1 times.
77 auto* p = loadSHLIBPlugin(shlibName, name);
791
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 67 times.
76 if (p == nullptr) {
792
2/4
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
9 WarningHolder::AddWarning(5, "Could not load plugin " + name);
793 }
794 76 return p;
795 78 }
796 }
797 ✗ delete spl;
798 }
799 ✗ return nullptr;
800 133 }
801
802 1286 FPPPlugins::Plugin* PluginManager::loadSHLIBPlugin(const std::string& shlibName, const std::string& dirName) {
803 // If this path was loaded before and the file behind it has since been
804 // replaced - an upgrade between an unloadPlugin() and a loadPlugin() - it
805 // cannot simply be reopened. dlopen() matches an already-loaded object by
806 // NAME before it ever compares the file, so the same path hands back the
807 // copy still mapped and the new build is ignored: the upgrade appears to
808 // succeed while the old code keeps running. Give the new file a name of its
809 // own to open, then unlink that name - the mapping keeps the inode alive, so
810 // nothing is left behind on disk.
811 //
812 // An UNCHANGED file has the opposite problem once that has happened: the
813 // plain path is still bound to the first build ever loaded from it, so
814 // opening it by that name brings the original build back - a second reload
815 // after an upgrade silently reverted to the pre-upgrade code. Reopen it
816 // under the name it was last opened as, which the loader matches to the
817 // build that is actually current.
818
1/2
✓ Branch 0 taken 1286 times.
✗ Branch 1 not taken.
1286 std::string openPath = shlibName;
819 struct stat sb;
820 1286 bool haveStat = (stat(shlibName.c_str(), &sb) == 0);
821
1/2
✓ Branch 0 taken 1286 times.
✗ Branch 1 not taken.
1286 auto known = mLoadedShlibInodes.find(shlibName);
822
8/8
✓ Branch 0 taken 1273 times.
✓ Branch 1 taken 13 times.
✓ Branch 2 taken 144 times.
✓ Branch 3 taken 1129 times.
✓ Branch 4 taken 143 times.
✓ Branch 5 taken 1 times.
✓ Branch 6 taken 143 times.
✓ Branch 7 taken 1143 times.
1286 if (haveStat && known != mLoadedShlibInodes.end() && known->second.inode == sb.st_ino) {
823
1/2
✓ Branch 0 taken 143 times.
✗ Branch 1 not taken.
143 openPath = known->second.openedAs;
824
6/6
✓ Branch 0 taken 1130 times.
✓ Branch 1 taken 13 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 1129 times.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 1142 times.
1143 } else if (haveStat && known != mLoadedShlibInodes.end()) {
825 static int generation = 0;
826
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 std::string genPath = shlibName + ".gen" + std::to_string(++generation);
827 1 unlink(genPath.c_str());
828
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (link(shlibName.c_str(), genPath.c_str()) == 0) {
829
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 openPath = genPath;
830 } else {
831 ✗ LogWarn(VB_PLUGIN, "%s was replaced on disk but a private name could not be made (%s); "
832 "the previously loaded build stays in use until fppd restarts\n",
833 shlibName.c_str(), FPPstrerror(errno));
834 }
835 1 }
836
837 1286 void* handle = dlopen(openPath.c_str(), RTLD_NOW);
838
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1284 times.
1286 if (openPath != shlibName) {
839 2 unlink(openPath.c_str());
840 }
841
3/4
✓ Branch 0 taken 1273 times.
✓ Branch 1 taken 13 times.
✓ Branch 2 taken 1273 times.
✗ Branch 3 not taken.
1286 if (handle != nullptr && haveStat) {
842
1/2
✓ Branch 0 taken 1273 times.
✗ Branch 1 not taken.
1273 mLoadedShlibInodes[shlibName] = { sb.st_ino, openPath };
843 }
844
2/2
✓ Branch 0 taken 13 times.
✓ Branch 1 taken 1273 times.
1286 if (handle == nullptr) {
845
9/22
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 13 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 13 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 13 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 13 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 13 times.
✗ Branch 11 not taken.
✓ Branch 12 taken 13 times.
✗ Branch 13 not taken.
✓ Branch 14 taken 13 times.
✗ Branch 15 not taken.
✓ Branch 16 taken 13 times.
✗ Branch 17 not taken.
✗ Branch 18 not taken.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
✗ Branch 21 not taken.
13 if (!FileExists(shlibName) && !FileExists(getFPPDDir("/" + shlibName))) {
846
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 LogErr(VB_PLUGIN, "Failed to find shlib %s\n", shlibName.c_str());
847 }
848 13 char* er = dlerror();
849
1/2
✓ Branch 0 taken 13 times.
✗ Branch 1 not taken.
13 LogErr(VB_PLUGIN, "Failed to load shlib: %s\n", er);
850 13 return nullptr;
851 }
852 FPPPlugins::Plugin* (*fptr)();
853 1273 *(void**)(&fptr) = dlsym(handle, "createPlugin");
854
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1271 times.
1273 if (fptr == nullptr) {
855
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 LogErr(VB_PLUGIN, "Failed to find createPlugin() function in shlib %s\n", shlibName.c_str());
856
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 WarningHolder::AddWarning(5, "Could not load plugin " + shlibName + " (missing createPlugin entry point)");
857 2 dlclose(handle);
858 2 return nullptr;
859 }
860
861 // Check plugin API version to prevent crashes from ABI-incompatible plugins.
862 //
863 // dlsym() on a handle searches the object AND its DT_NEEDED dependencies, and
864 // every plugin links -lfpp. libfpp.so also defines this weak symbol (Plugin.h
865 // emits it into every TU that includes it), so a plugin predating the version
866 // mechanism resolves straight through to FPP's own copy and reports a version
867 // that trivially matches. Verified on Debian 13/aarch64: without the
868 // provenance check below, a .so defining no version symbol is accepted.
869 // So confirm the symbol actually lives in the plugin by comparing the shared
870 // object it was found in against the one createPlugin() came from - that entry
871 // point is never defined by libfpp, so it identifies the plugin itself.
872 int (*vfptr)();
873 1271 *(void**)(&vfptr) = dlsym(handle, "fpp_plugin_api_version");
874 Dl_info versionInfo;
875 Dl_info pluginInfo;
876
1/2
✓ Branch 0 taken 1271 times.
✗ Branch 1 not taken.
1271 bool ownVersionSymbol = vfptr != nullptr &&
877
1/2
✓ Branch 0 taken 1271 times.
✗ Branch 1 not taken.
2542 dladdr((void*)vfptr, &versionInfo) != 0 &&
878
1/2
✓ Branch 0 taken 1271 times.
✗ Branch 1 not taken.
3813 dladdr((void*)fptr, &pluginInfo) != 0 &&
879
2/2
✓ Branch 0 taken 1270 times.
✓ Branch 1 taken 1 times.
1271 versionInfo.dli_fbase == pluginInfo.dli_fbase;
880
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1270 times.
1271 if (!ownVersionSymbol) {
881
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogErr(VB_PLUGIN, "Plugin %s was compiled against an older FPP API and is not compatible. Please update and rebuild the plugin.\n", shlibName.c_str());
882
3/6
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
1 WarningHolder::AddWarning(5, "Could not load plugin " + shlibName + " (built against an older FPP API - rebuild required)");
883 1 dlclose(handle);
884 1 return nullptr;
885 }
886
1/2
✓ Branch 0 taken 1270 times.
✗ Branch 1 not taken.
1270 int pluginVersion = vfptr();
887
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1270 times.
1270 if (pluginVersion != FPP_PLUGIN_API_VERSION) {
888 ✗ LogErr(VB_PLUGIN, "Plugin %s API version %d does not match FPP API version %d. Please update and rebuild the plugin.\n", shlibName.c_str(), pluginVersion, FPP_PLUGIN_API_VERSION);
889 ✗ WarningHolder::AddWarning(5, "Could not load plugin " + shlibName + " (API version mismatch - rebuild required)");
890 ✗ dlclose(handle);
891 ✗ return nullptr;
892 }
893
894 // Second, independent gate: compare the plugin's compiled-in sizeof() for
895 // the two types it shares layout with against ours. The version number
896 // above only helps if somebody remembers to bump it, and twice now a member
897 // was added to CommandArg without one - a plugin then push_backs onto
898 // Command::args with its own smaller node size while Command::~Command
899 // destroys those nodes with the larger one, running ~string past the end of
900 // the plugin's allocation. This catches that even when the versions agree.
901 // Both types are pimpl'd as of version 5 so these should now never move,
902 // which makes this cheap to keep and loud if the freeze ever slips.
903 //
904 // Same provenance trick as above, but inverted: here a symbol resolving to
905 // libfpp's own copy is the expected case for a plugin that never includes
906 // Commands.h. It registers no commands, so there is no layout to disagree
907 // about and nothing to check.
908 5076 auto abiSizeMatches = [&](const char* symbol, unsigned int coreSize, const char* what) {
909 unsigned int (*sfptr)();
910 5076 *(void**)(&sfptr) = dlsym(handle, symbol);
911 Dl_info sizeInfo;
912
5/6
✓ Branch 0 taken 5064 times.
✓ Branch 1 taken 12 times.
✓ Branch 2 taken 5064 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 2430 times.
✓ Branch 5 taken 2646 times.
10140 if (sfptr == nullptr || dladdr((void*)sfptr, &sizeInfo) == 0 ||
913
2/2
✓ Branch 0 taken 2418 times.
✓ Branch 1 taken 2646 times.
5064 sizeInfo.dli_fbase != pluginInfo.dli_fbase) {
914 2430 return true; // not the plugin's own symbol - nothing to compare
915 }
916
1/2
✓ Branch 0 taken 2646 times.
✗ Branch 1 not taken.
2646 unsigned int pluginSize = sfptr();
917
2/2
✓ Branch 0 taken 2643 times.
✓ Branch 1 taken 3 times.
2646 if (pluginSize == coreSize) {
918 2643 return true;
919 }
920
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 LogErr(VB_PLUGIN, "Plugin %s was built with a %u byte %s but FPP uses %u bytes. Please update and rebuild the plugin.\n",
921 shlibName.c_str(), pluginSize, what, coreSize);
922
5/10
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 3 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 3 times.
✗ Branch 9 not taken.
3 WarningHolder::AddWarning(5, "Could not load plugin " + shlibName + " (" + what + " ABI mismatch - rebuild required)");
923 3 return false;
924 1270 };
925
1/2
✓ Branch 0 taken 1270 times.
✗ Branch 1 not taken.
1270 if (!abiSizeMatches("fpp_command_arg_abi_size", (unsigned int)sizeof(Command::CommandArg), "Command::CommandArg") ||
926
2/4
✓ Branch 0 taken 1269 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1269 times.
✗ Branch 3 not taken.
1269 !abiSizeMatches("fpp_command_abi_size", (unsigned int)sizeof(Command), "Command") ||
927 // The VB_* macros resolve to a member offset within FPPLogger::INSTANCE,
928 // so a plugin that disagrees about this layout passes _LogWrite() a
929 // reference into the wrong facility - see the LAYOUT RULE in log.h.
930
7/8
✓ Branch 0 taken 1269 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 1269 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1268 times.
✓ Branch 5 taken 1 times.
✓ Branch 6 taken 3 times.
✓ Branch 7 taken 1267 times.
3807 !abiSizeMatches("fpp_logger_instance_abi_size", (unsigned int)sizeof(FPPLoggerInstance), "FPPLoggerInstance") ||
931
4/6
✓ Branch 0 taken 1268 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1268 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 1267 times.
1268 !abiSizeMatches("fpp_logger_abi_span", fpp_logger_abi_span(), "FPPLogger facility layout")) {
932 3 dlclose(handle);
933 3 return nullptr;
934 }
935
936
2/2
✓ Branch 0 taken 1266 times.
✓ Branch 1 taken 1 times.
1267 FPPPlugins::Plugin* p = fptr();
937
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1263 times.
1266 if (p == nullptr) {
938
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 LogErr(VB_PLUGIN, "Failed to create plugin from shlib %s\n", shlibName.c_str());
939
3/6
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
3 WarningHolder::AddWarning(5, "Could not load plugin " + shlibName + " (createPlugin returned no plugin)");
940 3 dlclose(handle);
941 3 return nullptr;
942 }
943
1/2
✓ Branch 0 taken 1263 times.
✗ Branch 1 not taken.
1263 mShlibHandles.push_back(handle);
944
1/2
✓ Branch 0 taken 1263 times.
✗ Branch 1 not taken.
1263 addPlugin(p);
945
946 // Opt-in: only a plugin that declares itself safe gets dlclose()d on unload.
947 // Same provenance check as the ABI gates above - the symbol has to be the
948 // plugin's own, not one picked up from a dependency.
949 1263 bool supportsUnload = false;
950 int (*unloadFptr)();
951 1263 *(void**)(&unloadFptr) = dlsym(handle, "fpp_plugin_supports_unload");
952 Dl_info unloadInfo;
953
5/6
✓ Branch 0 taken 32 times.
✓ Branch 1 taken 1231 times.
✓ Branch 2 taken 32 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 32 times.
✓ Branch 5 taken 1231 times.
1295 if (unloadFptr != nullptr && dladdr((void*)unloadFptr, &unloadInfo) != 0 &&
954
1/2
✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
32 unloadInfo.dli_fbase == pluginInfo.dli_fbase) {
955
1/2
✓ Branch 0 taken 32 times.
✗ Branch 1 not taken.
32 supportsUnload = (unloadFptr() == 1);
956 }
957
2/4
✓ Branch 0 taken 1263 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1263 times.
✗ Branch 3 not taken.
1263 mPluginDirNames[p] = dirName;
958
1/2
✓ Branch 0 taken 1263 times.
✗ Branch 1 not taken.
1263 mPluginLibraries[dirName] = { handle, supportsUnload };
959
3/4
✓ Branch 0 taken 32 times.
✓ Branch 1 taken 1231 times.
✓ Branch 2 taken 1263 times.
✗ Branch 3 not taken.
1263 LogDebug(VB_PLUGIN, "Plugin %s %s unloading\n", p->getName().c_str(),
960 supportsUnload ? "supports" : "does not support");
961 1263 return p;
962
1/2
✓ Branch 0 taken 1273 times.
✗ Branch 1 not taken.
2559 }
963 21 bool PluginManager::hasPlugins() {
964 21 return !mPlugins.empty();
965 }
966 1313 void PluginManager::addPlugin(FPPPlugins::Plugin* p) {
967
1/2
✓ Branch 0 taken 1313 times.
✗ Branch 1 not taken.
1313 mPlugins.push_back(p);
968
969
1/2
✓ Branch 0 taken 1313 times.
✗ Branch 1 not taken.
1313 FPPPlugin::PlaylistEventPlugin* pep = dynamic_cast<FPPPlugin::PlaylistEventPlugin*>(p);
970
2/2
✓ Branch 0 taken 57 times.
✓ Branch 1 taken 1256 times.
1313 if (pep) {
971
1/2
✓ Branch 0 taken 57 times.
✗ Branch 1 not taken.
57 std::lock_guard<std::mutex> lock(playlistPluginsLock);
972
1/2
✓ Branch 0 taken 57 times.
✗ Branch 1 not taken.
57 mPlaylistPlugins.push_back(pep);
973 57 }
974
1/2
✓ Branch 0 taken 1313 times.
✗ Branch 1 not taken.
1313 FPPPlugin::ChannelOutputPlugin* coep = dynamic_cast<FPPPlugin::ChannelOutputPlugin*>(p);
975
2/2
✓ Branch 0 taken 1213 times.
✓ Branch 1 taken 100 times.
1313 if (coep) {
976
1/2
✓ Branch 0 taken 1213 times.
✗ Branch 1 not taken.
1213 mChannelOutputPlugins.push_back(coep);
977 }
978
1/2
✓ Branch 0 taken 1313 times.
✗ Branch 1 not taken.
1313 FPPPlugin::ChannelDataPlugin* cdp = dynamic_cast<FPPPlugin::ChannelDataPlugin*>(p);
979
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 1306 times.
1313 if (cdp) {
980
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 std::lock_guard<std::mutex> lock(channelDataPluginsLock);
981
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 mChannelDataPlugins.push_back(cdp);
982 7 }
983
1/2
✓ Branch 0 taken 1313 times.
✗ Branch 1 not taken.
1313 FPPPlugin::APIProviderPlugin* app = dynamic_cast<FPPPlugin::APIProviderPlugin*>(p);
984
2/2
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 1303 times.
1313 if (app) {
985
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 mAPIProviderPlugins.push_back(app);
986 }
987 1313 }
988 template<class P, class F>
989 static void CallIntoPlugin(P* p, const char* what, F&& f);
990
991 152 void PluginManager::mediaCallback(const Json::Value& playlist, const MediaDetails& mediaDetails) {
992
1/2
✓ Branch 0 taken 152 times.
✗ Branch 1 not taken.
152 ForEachPlaylistPlugin(mPlaylistPlugins, [&](FPPPlugins::PlaylistEventPlugin* a) {
993
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
8 CallIntoPlugin(a, "mediaCallback", [&]() { a->mediaCallback(playlist, mediaDetails); });
994 4 });
995 152 }
996 1885 void PluginManager::registerApis() {
997
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 1885 times.
1890 for (auto a : mAPIProviderPlugins) {
998 // Some plugins register their commands here rather than in their
999 // constructor, so this needs recording too - see mPluginCommands.
1000
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 auto* plugin = dynamic_cast<FPPPlugins::Plugin*>(a);
1001 5 std::optional<CommandRecorder> recordCommands;
1002
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 if (plugin) {
1003
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 recordCommands.emplace(mPluginCommands, dirNameFor(plugin));
1004 }
1005
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 a->registerApis();
1006 5 }
1007 1885 }
1008 2325 void PluginManager::unregisterApis() {
1009
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 2325 times.
2332 for (auto a : mAPIProviderPlugins) {
1010
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 a->unregisterApis();
1011 }
1012 2325 }
1013 138483 void PluginManager::modifySequenceData(int ms, uint8_t* seqData) {
1014
1/2
✓ Branch 0 taken 138483 times.
✗ Branch 1 not taken.
138483 std::lock_guard<std::mutex> lock(channelDataPluginsLock);
1015
2/2
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 138483 times.
138497 for (auto a : mChannelDataPlugins) {
1016
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
14 a->modifySequenceData(ms, seqData);
1017 }
1018 138483 }
1019 138483 void PluginManager::modifyChannelData(int ms, uint8_t* seqData) {
1020
1/2
✓ Branch 0 taken 138483 times.
✗ Branch 1 not taken.
138483 std::lock_guard<std::mutex> lock(channelDataPluginsLock);
1021
2/2
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 138483 times.
138497 for (auto a : mChannelDataPlugins) {
1022
1/2
✓ Branch 0 taken 14 times.
✗ Branch 1 not taken.
14 a->modifyChannelData(ms, seqData);
1023 }
1024 138483 }
1025 1885 void PluginManager::addControlCallbacks(std::map<int, std::function<bool(int)>>& callbacks) {
1026
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 1885 times.
1890 for (auto a : mAPIProviderPlugins) {
1027 // Record which descriptors this plugin added, so unloadPlugin() can take
1028 // them back out of the epoll loop. The callbacks it installs are
1029 // std::functions whose code lives in the plugin, so leaving one
1030 // registered after the plugin is gone is a call into nothing.
1031 5 std::set<int> before;
1032
2/2
✓ Branch 0 taken 10 times.
✓ Branch 1 taken 5 times.
15 for (const auto& c : callbacks) {
1033
1/2
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
10 before.insert(c.first);
1034 }
1035
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 auto* plugin = dynamic_cast<FPPPlugins::Plugin*>(a);
1036 {
1037 // Several plugins register their commands from here - see
1038 // mPluginCommands. Scoped so it records before the fds are.
1039 5 std::optional<CommandRecorder> recordCommands;
1040
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 if (plugin) {
1041
2/4
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
5 recordCommands.emplace(mPluginCommands, dirNameFor(plugin));
1042 }
1043
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 a->addControlCallbacks(callbacks);
1044 5 }
1045
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 if (!plugin) {
1046 ✗ continue;
1047 }
1048
2/2
✓ Branch 0 taken 19 times.
✓ Branch 1 taken 5 times.
24 for (const auto& c : callbacks) {
1049
3/4
✓ Branch 0 taken 19 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✓ Branch 3 taken 10 times.
19 if (before.find(c.first) == before.end()) {
1050
3/6
✓ Branch 0 taken 9 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 9 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 9 times.
✗ Branch 5 not taken.
9 mPluginControlFds[dirNameFor(plugin)].push_back(c.first);
1051 }
1052 }
1053
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 }
1054 1885 }
1055
1056 // ---- Runtime load/unload ---------------------------------------------------
1057
1058 // The directory a plugin was installed from. Falls back to the plugin's own
1059 // name for anything not loaded through loadUserPlugin() - a script plugin, or a
1060 // shlib reached through findPlugin() - where the two are the same by
1061 // construction. See mPluginDirNames in Plugins.h for why this exists.
1062 1407 std::string PluginManager::dirNameFor(FPPPlugins::Plugin* plugin) const {
1063
1/2
✓ Branch 0 taken 1407 times.
✗ Branch 1 not taken.
1407 auto it = mPluginDirNames.find(plugin);
1064
2/4
✓ Branch 0 taken 1407 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1407 times.
✗ Branch 3 not taken.
1407 return it != mPluginDirNames.end() ? it->second : plugin->getName();
1065 }
1066
1067 // Resolves the name the Plugin Manager and the REST endpoints use - the
1068 // directory - to the loaded plugin, whatever the plugin calls itself.
1069 155 FPPPlugins::Plugin* PluginManager::findPluginByDir(const std::string& dirName) const {
1070
2/2
✓ Branch 0 taken 107 times.
✓ Branch 1 taken 81 times.
188 for (auto& a : mPlugins) {
1071
3/4
✓ Branch 0 taken 107 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 74 times.
✓ Branch 3 taken 33 times.
107 if (dirNameFor(a) == dirName) {
1072 74 return a;
1073 }
1074 }
1075 81 return nullptr;
1076 }
1077
1078 1276 void PluginManager::noteChannelOutputCreated(FPPPlugins::ChannelOutputPlugin* plugin) {
1079
2/4
✓ Branch 0 taken 1276 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1276 times.
✗ Branch 3 not taken.
1276 if (auto* p = dynamic_cast<FPPPlugins::Plugin*>(plugin)) {
1080
2/4
✓ Branch 0 taken 1276 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1276 times.
✗ Branch 3 not taken.
1276 mPluginsWithOutputs.insert(dirNameFor(p));
1081 }
1082 1276 }
1083
1084 115 bool PluginManager::isPluginLoaded(const std::string& name) {
1085 115 return findPluginByDir(name) != nullptr;
1086 }
1087
1088 34 void PluginManager::startPlugin(FPPPlugins::Plugin* plugin) {
1089
1/2
✓ Branch 0 taken 34 times.
✗ Branch 1 not taken.
34 auto* api = dynamic_cast<FPPPlugins::APIProviderPlugin*>(plugin);
1090
2/2
✓ Branch 0 taken 29 times.
✓ Branch 1 taken 5 times.
34 if (!api) {
1091 29 return;
1092 }
1093 // The boot path records these in registerApis()/addControlCallbacks(); a
1094 // runtime load comes through here instead and has to do the same.
1095
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 const std::string dirName = dirNameFor(plugin);
1096
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 CommandRecorder recordCommands(mPluginCommands, dirName);
1097
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 api->registerApis();
1098
1099 // At boot these land in a map fppd hands to EPollManager once everything has
1100 // registered. That map is long gone by the time a plugin loads at runtime,
1101 // so collect into a local one and register the descriptors directly.
1102 5 std::map<int, std::function<bool(int)>> callbacks;
1103
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 api->addControlCallbacks(callbacks);
1104
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5 times.
5 for (auto& c : callbacks) {
1105 ✗ EPollManager::INSTANCE.addFileDescriptor(c.first, c.second);
1106 ✗ mPluginControlFds[dirName].push_back(c.first);
1107 }
1108 5 }
1109
1110 40 void PluginManager::detachPlugin(FPPPlugins::Plugin* plugin) {
1111 320 auto drop = [plugin](auto& vec) {
1112
2/2
✓ Branch 0 taken 53 times.
✓ Branch 1 taken 160 times.
426 for (auto it = vec.begin(); it != vec.end();) {
1113
3/4
✓ Branch 0 taken 53 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 44 times.
✓ Branch 3 taken 9 times.
106 if (dynamic_cast<FPPPlugins::Plugin*>(*it) == plugin) {
1114
1/2
✓ Branch 0 taken 44 times.
✗ Branch 1 not taken.
88 it = vec.erase(it);
1115 } else {
1116 18 ++it;
1117 }
1118 }
1119 360 };
1120 {
1121
1/2
✓ Branch 0 taken 40 times.
✗ Branch 1 not taken.
40 std::lock_guard<std::mutex> lock(playlistPluginsLock);
1122
1/2
✓ Branch 0 taken 40 times.
✗ Branch 1 not taken.
40 drop(mPlaylistPlugins);
1123 40 }
1124
1/2
✓ Branch 0 taken 40 times.
✗ Branch 1 not taken.
40 drop(mChannelOutputPlugins);
1125 {
1126
1/2
✓ Branch 0 taken 40 times.
✗ Branch 1 not taken.
40 std::lock_guard<std::mutex> lock(channelDataPluginsLock);
1127
1/2
✓ Branch 0 taken 40 times.
✗ Branch 1 not taken.
40 drop(mChannelDataPlugins);
1128 40 }
1129
1/2
✓ Branch 0 taken 40 times.
✗ Branch 1 not taken.
40 drop(mAPIProviderPlugins);
1130
1/2
✓ Branch 0 taken 48 times.
✗ Branch 1 not taken.
48 for (auto it = mPlugins.begin(); it != mPlugins.end(); ++it) {
1131
2/2
✓ Branch 0 taken 40 times.
✓ Branch 1 taken 8 times.
48 if (*it == plugin) {
1132
1/2
✓ Branch 0 taken 40 times.
✗ Branch 1 not taken.
40 mPlugins.erase(it);
1133 40 break;
1134 }
1135 }
1136 // The settings watch calls settingChanged() on the plugin, so it goes too --
1137 // and it has to go now, while nothing else holds this name: a reload
1138 // registers the new instance's watch under it before the old one is
1139 // deleted. See settingsUnwatched.
1140
5/10
✓ Branch 0 taken 40 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 40 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 40 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 40 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 40 times.
✗ Branch 9 not taken.
80 FileMonitor::INSTANCE.RemoveFile(plugin->getName(), FPP_DIR_CONFIG("/plugin." + plugin->getName()));
1141 {
1142
1/2
✓ Branch 0 taken 40 times.
✗ Branch 1 not taken.
40 std::lock_guard<std::mutex> lock(settingsUnwatchedLock);
1143
1/2
✓ Branch 0 taken 40 times.
✗ Branch 1 not taken.
40 settingsUnwatched.insert(plugin);
1144 40 }
1145 40 }
1146
1147 38 bool PluginManager::loadPlugin(const std::string& name, std::string& error) {
1148
2/4
✓ Branch 0 taken 38 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 38 times.
38 if (isPluginLoaded(name)) {
1149 ✗ return true; // already running; nothing to do
1150 }
1151
6/12
✓ Branch 0 taken 38 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 38 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 38 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 38 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 38 times.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✓ Branch 11 taken 38 times.
76 if (!DirectoryExists(FPP_DIR_PLUGIN("/" + name))) {
1152 ✗ error = "no plugin directory for " + name;
1153 ✗ return false;
1154 }
1155 // Plenty of plugins are web-UI only - no callbacks script, nothing for fppd
1156 // to run. That is a successful no-op, not a failure: reporting it as one
1157 // told the user to restart FPPD to enable something that needs neither.
1158
5/10
✓ Branch 0 taken 38 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 38 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 38 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 38 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 38 times.
✗ Branch 9 not taken.
38 std::string callbacks = FPP_DIR_PLUGIN("/" + name + "/callbacks");
1159
1/2
✓ Branch 0 taken 38 times.
✗ Branch 1 not taken.
38 bool haveCallbacks = FileExists(callbacks);
1160
2/2
✓ Branch 0 taken 152 times.
✓ Branch 1 taken 38 times.
190 for (const char* ext : { ".sh", ".pl", ".php", ".py" }) {
1161
7/12
✓ Branch 0 taken 33 times.
✓ Branch 1 taken 119 times.
✓ Branch 2 taken 33 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 33 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 33 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 33 times.
✓ Branch 9 taken 119 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
152 haveCallbacks = haveCallbacks || FileExists(callbacks + ext);
1162 }
1163
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 38 times.
38 if (!haveCallbacks) {
1164 ✗ LogDebug(VB_PLUGIN, "Plugin %s has no callbacks; nothing for fppd to load\n", name.c_str());
1165 ✗ return true;
1166 }
1167
1168 // A previous unloadPlugin() left the name in this set (the library stays
1169 // mapped), so clear it or loadUserPlugin() will not look at the plugin again.
1170
1/2
✓ Branch 0 taken 38 times.
✗ Branch 1 not taken.
38 mLoadedUserPlugins.erase(name);
1171
1172
2/2
✓ Branch 0 taken 37 times.
✓ Branch 1 taken 1 times.
38 FPPPlugins::Plugin* p = loadUserPlugin(name, true);
1173
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 34 times.
37 if (!p) {
1174 // loadUserPlugin() logs the specific reason - a missing callbacks script,
1175 // an ABI-version refusal from loadSHLIBPlugin(), a createPlugin() that
1176 // returned nothing.
1177
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 error = "plugin " + name + " did not load - see the log";
1178 3 return false;
1179 }
1180
1/2
✓ Branch 0 taken 34 times.
✗ Branch 1 not taken.
34 startPlugin(p);
1181
1/2
✓ Branch 0 taken 34 times.
✗ Branch 1 not taken.
34 LogInfo(VB_PLUGIN, "Loaded plugin %s\n", name.c_str());
1182 34 return true;
1183 38 }
1184
1185 40 bool PluginManager::unloadPlugin(const std::string& name, std::string& error) {
1186 // By directory name, which is what the Plugin Manager and the REST
1187 // endpoints pass - a plugin is free to call itself something else.
1188
1/2
✓ Branch 0 taken 40 times.
✗ Branch 1 not taken.
40 FPPPlugins::Plugin* plugin = findPluginByDir(name);
1189
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 40 times.
40 if (!plugin) {
1190 ✗ return true; // not loaded; the caller's goal already holds
1191 }
1192 // A channel output the plugin created is owned by the output system, not by
1193 // the plugin, and may be mid-show. Nothing here can take it back, so refuse
1194 // rather than leave an output running against a destroyed plugin. Keyed on
1195 // having actually produced one - merely implementing ChannelOutputPlugin
1196 // says nothing, since the FPPPlugin convenience base inherits all four
1197 // plugin interfaces.
1198
2/4
✓ Branch 0 taken 40 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 40 times.
40 if (mPluginsWithOutputs.find(name) != mPluginsWithOutputs.end()) {
1199 ✗ error = name + " provides a channel output in use and can only be removed by restarting";
1200 ✗ return false;
1201 }
1202
1203 // Order matters: stop inbound HTTP first (unregisterApis does not return
1204 // until no request is inside the plugin's handlers and they are destroyed),
1205 // then let the plugin stop its own threads and timers while it is still a
1206 // whole object, then stop calling it, then destroy it.
1207
3/4
✓ Branch 0 taken 40 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✓ Branch 3 taken 37 times.
40 if (auto* api = dynamic_cast<FPPPlugins::APIProviderPlugin*>(plugin)) {
1208
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 api->unregisterApis();
1209 }
1210 // Withdraw the plugin's epoll descriptors BEFORE shutdown(), while they are
1211 // still open: a plugin that closes them itself in shutdown() would make the
1212 // EPOLL_CTL_DEL fail, and the callback - which lives in the plugin - has to
1213 // come out of the map either way.
1214
1/2
✓ Branch 0 taken 40 times.
✗ Branch 1 not taken.
40 auto fds = mPluginControlFds.find(name);
1215
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 40 times.
40 if (fds != mPluginControlFds.end()) {
1216 ✗ for (int fd : fds->second) {
1217 ✗ EPollManager::INSTANCE.removeFileDescriptor(fd);
1218 }
1219 ✗ mPluginControlFds.erase(fds);
1220 }
1221
1222
1/2
✓ Branch 0 taken 40 times.
✗ Branch 1 not taken.
40 std::function<bool()> ready = plugin->shutdown();
1223
1224 // Anything the plugin still has in flight holds a callback that lives in its
1225 // library. shutdown() cannot cancel those - they are owned by CurlManager -
1226 // so drop them here. Requests the plugin did not tag with its name are
1227 // invisible to this, which is exactly why unmapping is opt-in below.
1228
1/2
✓ Branch 0 taken 40 times.
✗ Branch 1 not taken.
40 CurlManager::INSTANCE.cancelRequests(name);
1229
1230 // Backstop for commands the plugin did not take back itself. A C++ plugin
1231 // owns what it registers and is expected to withdraw and delete it in
1232 // shutdown() (see Plugin.h) - this is the safety net for one that forgets,
1233 // plus the only owner of the commands FPP itself registered from a script
1234 // plugin's descriptions.json. A Command subclass declared in a plugin has
1235 // its vtable in that plugin's .so, so anything left has to go before the
1236 // library does.
1237
1/2
✓ Branch 0 taken 40 times.
✗ Branch 1 not taken.
40 auto cmds = mPluginCommands.find(name);
1238
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 33 times.
40 if (cmds != mPluginCommands.end()) {
1239 // What is registered under each name NOW, so a command the plugin
1240 // already withdrew itself (in shutdown(), which has just run) is not
1241 // deleted twice, and one that something else has since re-registered
1242 // under the same name is left to its new owner.
1243 7 std::map<std::string, Command*> current;
1244
3/4
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 453 times.
✓ Branch 3 taken 7 times.
460 for (const auto& c : CommandManager::INSTANCE.getRegisteredCommands()) {
1245
1/2
✓ Branch 0 taken 453 times.
✗ Branch 1 not taken.
453 current.emplace(c.first, c.second);
1246 7 }
1247 7 std::vector<std::string> leftBehind;
1248
2/2
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 7 times.
14 for (const auto& c : cmds->second) {
1249
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 auto it = current.find(c.first);
1250
5/6
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 4 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 3 times.
✓ Branch 4 taken 4 times.
✓ Branch 5 taken 3 times.
7 if (it == current.end() || it->second != c.second) {
1251 4 continue;
1252 }
1253
3/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 1 times.
3 if (dynamic_cast<ScriptCommand*>(c.second)) {
1254 // FPP's own class, so it may outlive this call: a script can
1255 // run for any length of time, and its run must finish on a
1256 // live object.
1257
1/2
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
2 CommandManager::INSTANCE.retireCommand(c.second);
1258 } else {
1259
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 CommandManager::INSTANCE.removeCommand(c.first); // unregisters only
1260
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 delete c.second;
1261 }
1262
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 leftBehind.push_back(c.first);
1263 }
1264 // Say so rather than quietly covering for it: a C++ plugin reaching this
1265 // has a shutdown() that does not withdraw what it registered, and the
1266 // net only reaches the commands added while it was loading. (Script
1267 // plugins are expected here - FPP registered those from their
1268 // descriptions.json and owns them.)
1269
7/8
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✓ Branch 5 taken 2 times.
✓ Branch 6 taken 1 times.
✓ Branch 7 taken 6 times.
7 if (!leftBehind.empty() && mPluginLibraries.find(name) != mPluginLibraries.end()) {
1270 1 std::string list;
1271
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
2 for (const auto& n : leftBehind) {
1272
3/6
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
1 list += (list.empty() ? "" : ", ") + n;
1273 }
1274
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogWarn(VB_PLUGIN, "Plugin %s left %d command(s) registered at unload (%s); it should withdraw and delete them in shutdown()\n",
1275 name.c_str(), (int)leftBehind.size(), list.c_str());
1276 1 }
1277
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 mPluginCommands.erase(cmds);
1278 7 }
1279
1280 // Detach now: from here nothing calls into the plugin, so as far as the rest
1281 // of FPP is concerned it is unloaded, whatever the settling time says.
1282
1/2
✓ Branch 0 taken 40 times.
✗ Branch 1 not taken.
40 detachPlugin(plugin);
1283
1/2
✓ Branch 0 taken 40 times.
✗ Branch 1 not taken.
40 mLoadedUserPlugins.erase(name);
1284
1285 // Unmapping is opt-in. A plugin that declared FPP_PLUGIN_SUPPORTS_UNLOAD has
1286 // asserted it leaves nothing behind pointing into its library; without that
1287 // the mapping stays, which costs address space and nothing else.
1288 40 void* handle = nullptr;
1289 40 bool unmap = false;
1290
1/2
✓ Branch 0 taken 40 times.
✗ Branch 1 not taken.
40 auto lib = mPluginLibraries.find(name);
1291 40 bool hadLibrary = (lib != mPluginLibraries.end());
1292
2/2
✓ Branch 0 taken 8 times.
✓ Branch 1 taken 32 times.
40 if (lib != mPluginLibraries.end()) {
1293
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 3 times.
8 if (lib->second.supportsUnload) {
1294 5 handle = lib->second.handle;
1295 5 unmap = true;
1296
1/2
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
6 for (auto it = mShlibHandles.begin(); it != mShlibHandles.end(); ++it) {
1297
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 1 times.
6 if (*it == handle) {
1298
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 mShlibHandles.erase(it); // or Cleanup() would dlclose it a second time
1299 5 break;
1300 }
1301 }
1302
1/2
✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
5 mPluginLibraries.erase(lib);
1303 // The library leaves the link map when it is closed, so a later load
1304 // of the same path opens whatever is on disk - no generation link
1305 // needed for it.
1306
2/2
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 5 times.
11 for (auto it = mLoadedShlibInodes.begin(); it != mLoadedShlibInodes.end();) {
1307
5/8
✓ Branch 0 taken 6 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 5 times.
✓ Branch 5 taken 1 times.
✓ Branch 6 taken 5 times.
✗ Branch 7 not taken.
6 it = (it->first.find("/" + name + "/") != std::string::npos) ? mLoadedShlibInodes.erase(it) : ++it;
1308 }
1309 }
1310 }
1311
1312 40 PendingUnload pending;
1313 40 pending.plugin = plugin;
1314 40 pending.handle = handle;
1315 40 pending.unmap = unmap;
1316 40 pending.hadLibrary = hadLibrary;
1317
1/2
✓ Branch 0 taken 40 times.
✗ Branch 1 not taken.
40 pending.name = name;
1318 40 pending.ready = std::move(ready);
1319 // Cap the wait so a predicate that never returns true delays teardown
1320 // rather than holding the plugin forever.
1321
1/2
✓ Branch 0 taken 40 times.
✗ Branch 1 not taken.
40 pending.deadlineMS = GetMonotonicTimeMS() + 60000;
1322
1323
2/2
✓ Branch 0 taken 39 times.
✓ Branch 1 taken 1 times.
40 if (!pending.ready) {
1324
1/2
✓ Branch 0 taken 39 times.
✗ Branch 1 not taken.
39 finishUnload(pending);
1325 39 return true;
1326 }
1327 // Still finishing. A load arriving in the meantime is fine: it constructs a
1328 // fresh object, and the dlopen it does raises the library's reference count
1329 // above what this later drops.
1330
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 mPendingUnloads.push_back(std::move(pending));
1331
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogInfo(VB_PLUGIN, "Unloaded plugin %s (waiting for it to finish)\n", name.c_str());
1332
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 pollPendingUnload(name);
1333 1 return true;
1334 40 }
1335
1336 // Re-arms itself once a second until the plugin says it is done. Safe to
1337 // reschedule from inside the callback: a one-shot timer is removed from the list
1338 // before it fires, so addTimer() here adds a fresh entry rather than mutating
1339 // the one being run.
1340 4 void PluginManager::pollPendingUnload(const std::string& name) {
1341
5/10
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 4 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 4 times.
✗ Branch 9 not taken.
4 Timers::INSTANCE.addTimer("PluginUnload-" + name, GetTimeMS() + 1000, [this, name]() {
1342
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 for (auto it = mPendingUnloads.begin(); it != mPendingUnloads.end(); ++it) {
1343
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 if (it->name != name) {
1344 ✗ continue;
1345 }
1346 4 bool done = false;
1347
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 long long now = GetMonotonicTimeMS();
1348
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 if (now >= it->deadlineMS) {
1349 ✗ LogWarn(VB_PLUGIN, "Plugin %s never reported finished; tearing down anyway\n", name.c_str());
1350 ✗ done = true;
1351 } else {
1352 // The predicate is the plugin's code, which is why this runs
1353 // while the library is still mapped.
1354 try {
1355
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 done = it->ready();
1356 ✗ } catch (...) {
1357 ✗ LogWarn(VB_PLUGIN, "Plugin %s threw while reporting readiness; tearing down\n", name.c_str());
1358 ✗ done = true;
1359 ✗ }
1360 }
1361
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 3 times.
4 if (done) {
1362 1 PendingUnload p = std::move(*it);
1363
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 mPendingUnloads.erase(it);
1364
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 finishUnload(p);
1365 1 } else {
1366
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
3 pollPendingUnload(name);
1367 }
1368 4 return;
1369 }
1370 });
1371 4 }
1372
1373 40 void PluginManager::finishUnload(PendingUnload& p) {
1374 // Drop the predicate before the library goes: it is the plugin's code too.
1375 40 p.ready = nullptr;
1376 // Detached already, but a playlist or media callback that took its copy of
1377 // the list before then may still be inside the plugin on another thread.
1378 {
1379
1/2
✓ Branch 0 taken 40 times.
✗ Branch 1 not taken.
40 std::unique_lock<std::mutex> lock(playlistPluginsLock);
1380
2/4
✓ Branch 0 taken 40 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 40 times.
40 if (!playlistPluginCallDone.wait_for(lock, std::chrono::seconds(30), [&p]() {
1381
1/2
✓ Branch 0 taken 40 times.
✗ Branch 1 not taken.
40 return playlistPluginCallsInFlight.find(p.plugin) == playlistPluginCallsInFlight.end();
1382 })) {
1383 ✗ LogWarn(VB_PLUGIN, "Plugin %s still had a playlist callback running after 30 s; deleting it anyway\n", p.name.c_str());
1384 }
1385 40 }
1386 40 mPluginDirNames.erase(p.plugin); // so the map never holds a freed pointer
1387
1/2
✓ Branch 0 taken 40 times.
✗ Branch 1 not taken.
40 delete p.plugin; // the plugin's own full teardown
1388
3/4
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 35 times.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
40 if (p.unmap && p.handle) {
1389 5 dlclose(p.handle);
1390 5 LogInfo(VB_PLUGIN, "Unloaded plugin %s (library unmapped)\n", p.name.c_str());
1391
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 32 times.
35 } else if (p.hadLibrary) {
1392 3 LogInfo(VB_PLUGIN, "Unloaded plugin %s (library kept mapped)\n", p.name.c_str());
1393 } else {
1394 // A script plugin has no library of its own to keep or drop.
1395 32 LogInfo(VB_PLUGIN, "Unloaded plugin %s\n", p.name.c_str());
1396 }
1397 40 }
1398 // Plugin callbacks run on the main loop (MultiSync plugin packets arrive from
1399 // any LAN sender) and on API threads. Nothing above them catches, so a plugin
1400 // that throws would terminate fppd; contain it here and log which plugin it was.
1401 template<class P, class F>
1402 262 static void CallIntoPlugin(P* p, const char* what, F&& f) {
1403 try {
1404
2/2
✓ Branch 0 taken 126 times.
✓ Branch 1 taken 3 times.
262 f();
1405 12 } catch (...) {
1406
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
6 FPPPlugins::Plugin* named = dynamic_cast<FPPPlugins::Plugin*>(p);
1407
3/10
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 3 times.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
6 std::string name = named ? named->getName() : "(unnamed)";
1408 try {
1409 6 throw;
1410
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
12 } catch (const std::exception& ex) {
1411
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
6 LogErr(VB_PLUGIN, "Plugin %s threw from %s: %s\n", name.c_str(), what, ex.what());
1412 ✗ } catch (...) {
1413 ✗ LogErr(VB_PLUGIN, "Plugin %s threw a non-standard exception from %s\n", name.c_str(), what);
1414 }
1415 6 }
1416 258 }
1417
1418 4 void PluginManager::multiSyncData(const std::string& pn, uint8_t* data, int len) {
1419
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 4 times.
8 for (auto a : mPlugins) {
1420
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 if (a->getName() == pn) {
1421
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
8 CallIntoPlugin(a, "multiSyncData", [&]() { a->multiSyncData(data, len); });
1422 }
1423 }
1424 4 }
1425 42113 void PluginManager::playlistCallback(const Json::Value& playlist, const std::string& action, const std::string& section, int item) {
1426
1/2
✓ Branch 0 taken 42111 times.
✗ Branch 1 not taken.
42113 ForEachPlaylistPlugin(mPlaylistPlugins, [&](FPPPlugins::PlaylistEventPlugin* a) {
1427
1/2
✓ Branch 0 taken 121 times.
✗ Branch 1 not taken.
246 CallIntoPlugin(a, "playlistCallback", [&]() { a->playlistCallback(playlist, action, section, item); });
1428 121 });
1429 42111 }
1430 591 void PluginManager::playlistInserted(const std::string& playlist, const int position, int endPosition, bool immediate) {
1431
1/2
✓ Branch 0 taken 591 times.
✗ Branch 1 not taken.
591 ForEachPlaylistPlugin(mPlaylistPlugins, [&](FPPPlugins::PlaylistEventPlugin* a) {
1432 ✗ CallIntoPlugin(a, "playlistInserted", [&]() { a->playlistInserted(playlist, position, endPosition, immediate); });
1433 ✗ });
1434 591 }
1435
1436 // blocking
1437 4 void MediaCallback::run(const Json::Value& playlist, const MediaDetails& mediaDetails) {
1438 // Built before the fork, and the child only execs: see PlaylistCallback::run().
1439
3/6
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
4 std::string eventScript = FPP_DIR + "/scripts/eventScript";
1440
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 Json::Value root;
1441
1442
4/8
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 4 times.
✗ Branch 7 not taken.
4 root["type"] = playlist["currentEntry"]["type"];
1443
1444
8/26
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 4 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 4 times.
✗ Branch 10 not taken.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
✓ Branch 18 taken 4 times.
✗ Branch 19 not taken.
✓ Branch 20 taken 4 times.
✗ Branch 21 not taken.
✗ Branch 22 not taken.
✓ Branch 23 taken 4 times.
✗ Branch 24 not taken.
✗ Branch 25 not taken.
4 root["Sequence"] = playlist["currentEntry"]["type"].asString() == "both" ? playlist["currentEntry"]["sequence"]["sequenceName"].asString().c_str() : "";
1445
5/10
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 4 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 4 times.
✗ Branch 9 not taken.
8 root["Media"] = playlist["currentEntry"]["type"].asString() == "both"
1446
0/10
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✗ Branch 9 not taken.
4 ? playlist["currentEntry"]["media"]["mediaFilename"].asString().c_str()
1447
7/16
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 4 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 4 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 4 times.
✗ Branch 11 not taken.
✗ Branch 12 not taken.
✓ Branch 13 taken 4 times.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
8 : playlist["currentEntry"]["mediaFilename"].asString().c_str();
1448
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 if (!mediaDetails.title.empty()) {
1449 ✗ root["title"] = mediaDetails.title;
1450 }
1451
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 if (!mediaDetails.artist.empty()) {
1452 ✗ root["artist"] = mediaDetails.artist;
1453 }
1454
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 if (!mediaDetails.album.empty()) {
1455 ✗ root["album"] = mediaDetails.album;
1456 }
1457
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 if (mediaDetails.year) {
1458 ✗ root["year"] = std::to_string(mediaDetails.year);
1459 }
1460
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 if (!mediaDetails.comment.empty()) {
1461 ✗ root["comment"] = mediaDetails.comment;
1462 }
1463
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 if (mediaDetails.track) {
1464 ✗ root["track"] = std::to_string(mediaDetails.track);
1465 }
1466
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 if (!mediaDetails.genre.empty()) {
1467 ✗ root["genre"] = mediaDetails.genre;
1468 }
1469
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1 times.
4 if (mediaDetails.length) {
1470
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 root["length"] = std::to_string(mediaDetails.length);
1471 }
1472
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1 times.
4 if (mediaDetails.seconds) {
1473
2/4
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
3 root["seconds"] = std::to_string(mediaDetails.seconds);
1474 }
1475
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 4 times.
4 if (mediaDetails.minutes) {
1476 ✗ root["minutes"] = std::to_string(mediaDetails.minutes);
1477 }
1478
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
4 if (mediaDetails.bitrate) {
1479
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 root["bitrate"] = std::to_string(mediaDetails.bitrate);
1480 }
1481
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
4 if (mediaDetails.sampleRate) {
1482
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 root["sampleRate"] = std::to_string(mediaDetails.sampleRate);
1483 }
1484
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
4 if (mediaDetails.channels) {
1485
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 2 times.
✗ Branch 3 not taken.
2 root["channels"] = std::to_string(mediaDetails.channels);
1486 }
1487
1488
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 std::string pluginData = SaveJsonToString(root);
1489
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 LogDebug(VB_PLUGIN, "Media plugin data: %s\n", pluginData.c_str());
1490 int pid;
1491
1492
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 8 times.
4 if ((pid = fork()) == -1) {
1493 // Skip this callback, as LifecycleCallback does: exiting from here
1494 // took fppd down mid-show for one plugin's notification.
1495 ✗ LogErr(VB_PLUGIN, "Failed to fork for %s's media callback: %s\n", mName.c_str(), FPPstrerror(errno));
1496 ✗ return;
1497 }
1498
1499
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 4 times.
8 if (pid == 0) {
1500 4 execl(eventScript.c_str(), "eventScript", mFilename.c_str(), "--type", "media", "--data", pluginData.c_str(), NULL);
1501 4 _exit(EXIT_FAILURE);
1502 }
1503
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 LogExcess(VB_PLUGIN, "Media parent process, resuming work.\n");
1504
1/2
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
4 waitpid(pid, NULL, 0);
1505
3/6
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 4 times.
✗ Branch 5 not taken.
4 }
1506
1507 112 void PlaylistCallback::run(const Json::Value& playlist, const std::string& action, const std::string& section, int idx) {
1508 // Everything the child needs is built here, before the fork, and the
1509 // child only execs. fppd is threaded and these run on any thread (an
1510 // MQTT stop runs them on mosquitto's): a child that allocated or logged
1511 // could block forever on a lock another thread held at the moment of the
1512 // fork -- the logger's, an allocator's -- and this thread then waited for
1513 // it in waitpid() for good, the main loop with it.
1514
3/6
✓ Branch 0 taken 112 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 112 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 112 times.
✗ Branch 5 not taken.
112 std::string eventScript = FPP_DIR + "/scripts/eventScript";
1515
1/2
✓ Branch 0 taken 112 times.
✗ Branch 1 not taken.
112 Json::Value root = playlist;
1516
2/4
✓ Branch 0 taken 112 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 112 times.
✗ Branch 3 not taken.
112 root["Action"] = action;
1517
2/4
✓ Branch 0 taken 112 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 112 times.
✗ Branch 3 not taken.
112 root["Section"] = section;
1518
2/4
✓ Branch 0 taken 112 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 112 times.
✗ Branch 3 not taken.
112 root["Item"] = idx;
1519
2/4
✓ Branch 0 taken 112 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 112 times.
✗ Branch 3 not taken.
112 std::string pluginData = SaveJsonToString(root);
1520
1/2
✓ Branch 0 taken 112 times.
✗ Branch 1 not taken.
112 LogDebug(VB_PLUGIN, "Calling %s callback for playlist: %s\n", mName.c_str(), mFilename.c_str());
1521
1/2
✓ Branch 0 taken 112 times.
✗ Branch 1 not taken.
112 LogDebug(VB_PLUGIN, "Playlist plugin data: %s\n", pluginData.c_str());
1522
1523 int pid;
1524
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 224 times.
112 if ((pid = fork()) == -1) {
1525 ✗ LogErr(VB_PLUGIN, "Failed to fork for %s's playlist callback: %s\n", mName.c_str(), FPPstrerror(errno));
1526 ✗ return;
1527 }
1528
1529
2/2
✓ Branch 0 taken 112 times.
✓ Branch 1 taken 112 times.
224 if (pid == 0) {
1530 112 execl(eventScript.c_str(), "eventScript", mFilename.c_str(), "--type", "playlist", "--data", pluginData.c_str(), NULL);
1531 112 _exit(EXIT_FAILURE);
1532 }
1533
1/2
✓ Branch 0 taken 112 times.
✗ Branch 1 not taken.
112 LogExcess(VB_PLUGIN, "Playlist parent process, resuming work.\n");
1534
1/2
✓ Branch 0 taken 110 times.
✗ Branch 1 not taken.
112 waitpid(pid, NULL, 0);
1535
3/6
✓ Branch 0 taken 110 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 110 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 110 times.
✗ Branch 5 not taken.
110 }
1536 85 void LifecycleCallback::run(const std::string& lifecycle) {
1537 // Built before the fork, and the child only execs: see PlaylistCallback::run().
1538
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.
85 std::string eventScript = FPP_DIR + "/scripts/eventScript";
1539
1/2
✓ Branch 0 taken 85 times.
✗ Branch 1 not taken.
85 LogDebug(VB_PLUGIN, "Calling %s callback for lifecycle %s: %s\n", mName.c_str(), lifecycle.c_str(), mFilename.c_str());
1540 int pid;
1541
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 170 times.
85 if ((pid = fork()) == -1) {
1542 ✗ LogErr(VB_PLUGIN, "Failed to fork for %s's %s callback\n", mName.c_str(), lifecycle.c_str());
1543 ✗ return;
1544 }
1545
1546
2/2
✓ Branch 0 taken 85 times.
✓ Branch 1 taken 85 times.
170 if (pid == 0) {
1547 85 execl(eventScript.c_str(), "eventScript", mFilename.c_str(), "--type", "lifecycle", lifecycle.c_str(), NULL);
1548 85 _exit(EXIT_FAILURE);
1549 } else {
1550
1/2
✓ Branch 0 taken 85 times.
✗ Branch 1 not taken.
85 LogExcess(VB_PLUGIN, "Lifecycle parent process, resuming work.\n");
1551 // A plugin load or unload at runtime runs this on the main loop, so
1552 // a script that takes its time (starting or stopping a daemon) held
1553 // output for as long as it ran. Wait a bounded time, then let it
1554 // finish on its own; it isn't killed, since whatever it is starting
1555 // may be meant to outlive it.
1556 85 constexpr int LIFECYCLE_WAIT_MS = 5000;
1557
1/2
✓ Branch 0 taken 85 times.
✗ Branch 1 not taken.
85 long long deadline = GetMonotonicTimeMS() + LIFECYCLE_WAIT_MS;
1558 while (true) {
1559
1/2
✓ Branch 0 taken 1104 times.
✗ Branch 1 not taken.
1104 int r = waitpid(pid, NULL, WNOHANG);
1560
4/6
✓ Branch 0 taken 1104 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 84 times.
✓ Branch 3 taken 1020 times.
✗ Branch 4 not taken.
✓ Branch 5 taken 84 times.
1104 if (r == pid || (r < 0 && errno != EINTR)) {
1561 break;
1562 }
1563
3/4
✓ Branch 0 taken 1020 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 1019 times.
1020 if (GetMonotonicTimeMS() >= deadline) {
1564
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 LogWarn(VB_PLUGIN, "Plugin %s: %s callback still running after %d seconds; continuing without it\n",
1565 mName.c_str(), lifecycle.c_str(), LIFECYCLE_WAIT_MS / 1000);
1566 1 break;
1567 }
1568
1/2
✓ Branch 0 taken 1019 times.
✗ Branch 1 not taken.
1019 std::this_thread::sleep_for(std::chrono::milliseconds(10));
1569 1019 }
1570 }
1571
1/2
✓ Branch 0 taken 85 times.
✗ Branch 1 not taken.
85 }
1572