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