GCC Code Coverage Report


Directory: ./
File: Plugin.h
Date: 2026-10-29 08:11:58
Exec Total Coverage
Lines: 12 18 66.7%
Functions: 10 16 62.5%
Branches: 0 0 -%

Line Branch Exec Source
1 #pragma once
2 /*
3 * This file is part of the Falcon Player (FPP) and is Copyright (C)
4 * 2013-2022 by the Falcon Player Developers.
5 *
6 * The Falcon Player (FPP) is free software, and is covered under
7 * multiple Open Source licenses. Please see the included 'LICENSES'
8 * file for descriptions of what files are covered by each license.
9 *
10 * This source file is covered under the LGPL v2.1 as described in the
11 * included LICENSE.LGPL file.
12 */
13
14 #include <functional>
15 #include <map>
16 #include <string>
17
18 // For the FPPLogger layout fingerprints at the bottom of this file. They live
19 // here rather than in log.h because this is the header every plugin includes
20 // textually: log.h usually arrives through the PCH, and a weak definition that
21 // nothing in the translation unit references is not emitted from there.
22 #include "log.h"
23
24 #include "fpphttp_types.h"
25
26 // Increment this when the plugin ABI changes. The headers plugins compile in
27 // are marked "PLUGIN ABI:"; .claude/PLUGIN-ABI.md lists them and what each
28 // plugin uses. "ABI" is not just virtual method
29 // signatures - it is anything that changes the layout of a type a plugin can
30 // construct, destroy, or subclass. Adding a data member to Command::CommandArg
31 // counts, because plugins push_back into Command::args with the old sizeof while
32 // FPP destroys those nodes with the new one; adding a virtual to Command counts,
33 // because it shifts every later vtable slot.
34 //
35 // Version 4 (FPP 10.0): Command::CommandArg gained advanced/help/children/
36 // toggleStyle/toggleLabel, and Command briefly gained a mid-vtable
37 // disallowMultisync(). Plugins built against version 3 headers corrupt the heap
38 // in Command::~Command and misdispatch getDescription() into the plugin's run().
39 //
40 // Version 5 (FPP 10.0): Command::CommandArg gained a std::string section,
41 // inserted between help and children - so it both grew sizeof(CommandArg) and
42 // shifted children/toggleStyle/toggleLabel. Plugins built against version 4
43 // headers hit the same heap corruption version 4 was introduced to stop.
44 //
45 // Version 5 is intended to be the LAST bump this file needs for Command or
46 // CommandArg growth: both now carry a unique_ptr to an out-of-line struct
47 // (Command::Data / CommandArg::Ext, defined in Commands.cpp) and new state goes
48 // there, where plugins only ever see a pointer. Their sizes are additionally
49 // published as fingerprint symbols the loader compares, so a slip is refused at
50 // dlopen rather than corrupting the heap. Bump this only for a change the
51 // pimpls cannot absorb - a new virtual on Command, or a change to one of the
52 // frozen members.
53 //
54 // "ABI" is also not only about Command. FPPLoggerInstance (log.h) grew a
55 // crashRingCapture bool, taking it from 28 to 32 bytes on 32-bit ARM, and the
56 // VB_* macros expand to a member of FPPLogger::INSTANCE - so plugins built
57 // against the older header resolved VB_PLUGIN 52 bytes short of where libfpp put
58 // it and handed _LogWrite() a reference into the middle of another facility;
59 // formatting that facility's name ran strlen() over raw character bytes and
60 // aborted fppd on the plugin's first log line. That layout is fingerprinted
61 // below rather than version-bumped, deliberately: a bump would also refuse the
62 // plugin binaries that have since been rebuilt correctly. The consequence is
63 // that a plugin built before those fingerprints existed exports nothing to
64 // compare and is still loaded - the gate covers recurrence, not the binaries
65 // already in the field.
66 //
67 // Version 6 (FPP 10.0): FPPPlugins::Plugin gained a virtual shutdown(), which
68 // shifts every later vtable slot - a plugin built against version 5 headers
69 // would have FPP call shutdown() and land in whatever its multiSyncData() slot
70 // held. This is the teardown point plugin unloading needs: a plugin's
71 // destructor is too late to stop threads, because another thread calling in
72 // during destruction reads a half-destroyed object.
73 //
74 // Version 6 also removed the deprecated registerApis(httpserver::webserver*) /
75 // unregisterApis(httpserver::webserver*) virtuals and the httpserver:: source
76 // shims behind them, which shifts vtable slots again. That shim existed to let
77 // libhttpserver-era plugin source recompile unchanged against FPP 10; no listed
78 // plugin still needed it, and the version bump forces a rebuild regardless, so
79 // there was no longer a plugin it could carry across.
80 //
81 // Landed alongside it (no ABI impact, but this is the version to build against
82 // for it): FPPPlugins::registerPluginApi()/unregisterPluginApi() in fpphttp.h.
83 // Plugins must use those instead of drogon::app().registerHandler(), which
84 // cannot be undone - drogon has no route removal - and so pins a plugin's .so
85 // in memory for the life of the process. Registering through FPP keeps the
86 // route in libfpp and lets a replacement build of the plugin take it over.
87 #define FPP_PLUGIN_API_VERSION 6
88
89 // Plugins compiled with these headers will export their API version.
90 // weak linkage allows multiple TUs to define this; visibility ensures .so export.
91 //
92 // Alongside it, fingerprints for the FPPLogger layout described by the LAYOUT
93 // RULE in log.h - the same second-gate idea as the Command/CommandArg sizes in
94 // commands/Commands.h, and for the same reason: relying on a human to remember
95 // the version bump has visibly not worked. Each module computes these from its
96 // own copy of log.h, so a plugin that disagrees about where VB_* lives is
97 // refused at dlopen instead of handing _LogWrite() a reference into the middle
98 // of another facility. The span is measured rather than sizeof()'d because the
99 // two ways this breaks differ: growing FPPLoggerInstance and inserting a
100 // facility both move the later VB_* macros, and only the first shows up in a
101 // sizeof.
102 #ifdef __cplusplus
103 extern "C" {
104 1196 __attribute__((weak, visibility("default"))) int fpp_plugin_api_version() { return FPP_PLUGIN_API_VERSION; }
105 1196 __attribute__((weak, visibility("default"))) unsigned int fpp_logger_instance_abi_size() {
106 1196 return (unsigned int)sizeof(FPPLoggerInstance);
107 }
108 2464 __attribute__((weak, visibility("default"))) unsigned int fpp_logger_abi_span() {
109 2464 return (unsigned int)((const char*)&FPPLogger::INSTANCE.HTTP - (const char*)&FPPLogger::INSTANCE.General);
110 }
111 }
112 #endif
113
114 // Opt in to being dlclose()d when unloaded. Put FPP_PLUGIN_SUPPORTS_UNLOAD() at
115 // file scope in exactly one of the plugin's translation units.
116 //
117 // Without it FPP still unloads the plugin - routes disarmed, shutdown() called,
118 // registrations withdrawn, the object destroyed - but keeps the library mapped,
119 // because it cannot prove nothing still points into it. The macro is a plugin
120 // asserting that it does not leave anything behind that would outlive the .so:
121 //
122 // - shutdown() stops and JOINS every thread it started, and stops its timers.
123 // - Every CurlManager request it issues passes its plugin name as the owner
124 // argument, so FPP can cancel the ones still in flight.
125 // - Any epoll descriptors it registered are gone (FPP withdraws the ones it
126 // added through addControlCallbacks(); anything registered directly with
127 // EPollManager is the plugin's own to remove).
128 // - Every Command it registered with CommandManager::addCommand() has been
129 // withdrawn AND DELETED in shutdown(). A Command subclass declared in a
130 // plugin has its vtable in that plugin's .so, so one left registered is a
131 // call into an unmapped library. Note removeCommand() only UNREGISTERS -
132 // CommandManager owns what is left in the registry and deletes it at
133 // shutdown, so a plugin taking one back owns it again and must delete it:
134 //
135 // for (Command* c : myCommands) {
136 // CommandManager::INSTANCE.removeCommand(c); // unregisters only
137 // delete c;
138 // }
139 // myCommands.clear();
140 //
141 // FPP does keep a backstop: it records what appeared in the registry while
142 // the plugin was loading and, at unload, deletes anything the plugin left
143 // behind. That is a safety net for a plugin that forgets, not the contract
144 // - it only covers the load window, so a command registered later (from a
145 // request handler or a thread) is the plugin's alone. It also only fires
146 // for a command still registered under the same name AND pointer, so a
147 // plugin doing its own cleanup is never double-freed.
148 // - It registered HTTP routes through FPPPlugins::registerPluginApi() only.
149 // - Anything it handed to a drogon/trantor event loop has finished, or it
150 // returns a settling time from shutdown() long enough that it will have.
151 // This is the case to think hardest about: runInLoop()/queueInLoop() are
152 // obvious, but so is any drogon client object - a WebSocketClient queues ITS
153 // handlers, which are the plugin's code, onto a loop whenever a network
154 // event arrives, and stopping the client does not close that window because
155 // the socket-closed callback is queued right afterwards. Trantor offers no
156 // way to enumerate or cancel what is already queued, so there is nothing to
157 // wait on: draining every loop in shutdown() was measured to still take a
158 // SIGSEGV inside doRunInLoopFuncs() moments after dlclose(). A settling time
159 // is what makes that survivable.
160 //
161 // Get one of those wrong and the failure is a jump into unmapped memory, so the
162 // default is to keep the mapping.
163 #define FPP_PLUGIN_SUPPORTS_UNLOAD() \
164 extern "C" __attribute__((visibility("default"))) int fpp_plugin_supports_unload() { \
165 return 1; \
166 }
167
168 #include "fpp-json-fwd.h"
169
170 class MediaDetails;
171 class ChannelOutput;
172
173 namespace FPPPlugins
174 {
175 class Plugin {
176 public:
177 Plugin(const std::string& n);
178 Plugin(const std::string& n, bool monitorSettings);
179 virtual ~Plugin();
180
181 2395 const std::string& getName() const { return name; }
182
183 // A plugin can call PluginManager::INSTANCE.multiSyncData(pluginName, data, len);
184 // with data and that data is multisynced out to all the remotes. If the plugin
185 // is installed and running on the remote, it will get that data in via this method
186 1 virtual void multiSyncData(const uint8_t* data, int len) {}
187
188 // Called by FPP once the plugin's HTTP routes have been disarmed and
189 // before the plugin object is destroyed. Quiesce here: stop threads and
190 // join them, stop timers, remove event handlers and commands, close
191 // connections.
192 //
193 // Destructors run too late for this: another thread calling in while the
194 // object is being destroyed reads a half-destroyed plugin. Doing it here
195 // gives a point where FPP guarantees no further calls in.
196 //
197 // Return an empty function when teardown is complete on return.
198 // Otherwise return a predicate FPP polls on the main loop about once a
199 // second, and which returns true when the plugin is finished. FPP
200 // detaches the plugin immediately either way - nothing calls into it
201 // again and its routes are gone - but waits for the predicate before
202 // DESTROYING it and (if the plugin opted into unmapping, see
203 // FPP_PLUGIN_SUPPORTS_UNLOAD) closing the library.
204 //
205 // That split lets shutdown() be "ask everything to stop" with the real
206 // teardown in the destructor once it actually has: a media pipeline that
207 // unwinds asynchronously reports true when it is done, rather than the
208 // caller guessing a duration. Anything arriving in the meantime finds a
209 // live, quiesced object instead of freed memory.
210 //
211 // A plugin that cannot observe when it is finished can return a deadline
212 // instead:
213 //
214 // long long until = GetTimeMS() + 5000;
215 // return [until]() { return GetTimeMS() >= until; };
216 //
217 // which is what the drogon/trantor case needs - a stopped WebSocket
218 // client can still have its handlers queued onto a loop by a socket
219 // event arriving right afterwards, and trantor offers no way to see or
220 // cancel what is queued, so there is nothing to poll and only elapsed
221 // time helps. Waiting turns a near-certain race into one needing an
222 // event seconds late; it narrows the window rather than closing it.
223 //
224 // The predicate itself lives in the plugin, so FPP destroys it before
225 // closing the library. FPP gives up waiting after 60s and tears down
226 // anyway, so a predicate that never returns true delays teardown, it
227 // does not leak the plugin forever.
228 1286 virtual std::function<bool()> shutdown() { return nullptr; }
229
230 protected:
231 void reloadSettings();
232 ✗ virtual void settingChanged(const std::string& key, const std::string& value) {}
233
234 std::string name;
235 std::map<std::string, std::string> settings;
236 };
237
238 class ChannelOutputPlugin {
239 public:
240 1196 ChannelOutputPlugin() {}
241 2392 virtual ~ChannelOutputPlugin() {}
242
243 // A plugin can provide an implementation of a channel output
244 ✗ virtual ChannelOutput* createChannelOutput(unsigned int startChannel, unsigned int channelCount) { return nullptr; }
245 };
246
247 class PlaylistEventPlugin {
248 public:
249 128 PlaylistEventPlugin() {}
250 252 virtual ~PlaylistEventPlugin() {}
251
252 // Touch points for various triggers within FPP
253 ✗ virtual void eventCallback(const char* id, const char* impetus) {}
254 ✗ virtual void mediaCallback(const Json::Value& playlist, const MediaDetails& mediaDetails) {}
255 ✗ virtual void playlistCallback(const Json::Value& playlist, const std::string& action, const std::string& section, int item) {}
256 ✗ virtual void playlistInserted(const std::string& playlist, const int position, int endPosition, bool immediate) {}
257 };
258
259 class ChannelDataPlugin {
260 public:
261 ChannelDataPlugin() {}
262 virtual ~ChannelDataPlugin() {}
263
264 // direct access to channel data.
265 // modifySequenceData is immedately after data is loaded/recieved (before overlays)
266 virtual void modifySequenceData(int ms, uint8_t* seqData) {}
267 // modifyChannelData is immediately before sending to outputs (after overlays)
268 virtual void modifyChannelData(int ms, uint8_t* seqData) {}
269 };
270
271 class APIProviderPlugin {
272 public:
273 APIProviderPlugin() {}
274 virtual ~APIProviderPlugin() {}
275
276 // Register HTTP routes with FPPPlugins::registerPluginApi() (fpphttp.h),
277 // and give them back in unregisterApis() with unregisterPluginApi().
278 virtual void registerApis() {}
279 virtual void unregisterApis() {}
280
281 virtual void addControlCallbacks(std::map<int, std::function<bool(int)>>& callbacks) {}
282 };
283 }
284
285 class FPPPlugin : public FPPPlugins::Plugin, public FPPPlugins::PlaylistEventPlugin, public FPPPlugins::ChannelOutputPlugin, public FPPPlugins::ChannelDataPlugin, public FPPPlugins::APIProviderPlugin {
286 public:
287 FPPPlugin(const std::string& n) :
288 FPPPlugins::Plugin(n) {}
289 FPPPlugin(const std::string& n, bool monitorSettings) :
290 FPPPlugins::Plugin(n, monitorSettings) {}
291 virtual ~FPPPlugin() {}
292 };
293