Files
unleashed-firmware/lib/flipper_application/plugins/plugin_manager.h
T
Mykhailo ShevchukandClaude Opus 5 944219d0ba Plugin loader: skip what it cannot load, and let a scan filter by file name (#1133)
* FlipperApplication: skip a plugin that fails to load instead of ending the scan

plugin_manager_load_all() broke out of the directory loop on the first file it
could not load, so a single stray or stale .fal silently cost every plugin
listed after it. It then returned PluginManagerErrorNone unconditionally, which
made every caller's error check dead code - including the one in the Sub-GHz
radio device registry, whose "Failed to load all libs" could never fire.

Skip the file and carry on, and return the first error the scan hit, so a
caller can tell a complete load from a partial one.

An app id mismatch is deliberately not one of those errors. A scan selects, and
a plugin belonging to another app is a normal thing to meet in a shared
directory - reporting it would hand the caller an error for a folder that is
working exactly as intended. That verdict belongs with the check itself rather
than at the call site, so the body of plugin_manager_load_single() moves into a
static helper that knows whether it is scanning: naming one file and getting a
mismatch is still an error, meeting one mid-scan is logged at debug.

Both examples checked the result and gave up, which was unreachable until now
and would have thrown away the plugins that did load - the opposite of the
point. They are the template a third-party plugin host gets copied from, so
they log and carry on instead, and the do/while(0) whose only break this was
goes with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* FlipperApplication: tell a failed directory read from the end of the directory

storage_dir_read() returns false both when it has handed back the last entry
and when the read itself failed - storage_ext_dir_read() sets FSE_NOT_EXIST for
the former and leaves FSE_NOT_READY, FSE_INTERNAL and friends for the latter,
so the only way to tell them apart is storage_file_get_error().

The scan loop treated the two the same, so a card pulled or a controller
hiccup partway through left a half-populated manager reporting complete
success. Check the error before the directory is closed, and report a read that
ended the scan early.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* FlipperApplication: let a plugin directory scan filter by file name prefix

A plugin's app id only becomes readable once its whole image has been mapped
and relocated, its init arrays run and its entry point called - see
flipper_application_plugin_get_descriptor(). So a scan of a directory shared
with plugins of another kind pays for a foreign image in full, and runs its
constructors against an API interface it was not built for, before it can tell
the plugin is not the one it wanted. And apps_data/<parent appid>/plugins is
fbt's default location for every plugin of an app, so sharing is the norm.

Add plugin_manager_load_all_prefixed(), which takes an optional file name
prefix and passes over anything that does not start with it. Same idea as
CliCommandExternalConfig::fal_prefix in the CLI registry, which has its own
loader for exactly this reason.

plugin_manager_load_all() keeps its signature and becomes the NULL-prefix case,
so nothing already built has to change. API 88.6 -> 88.7. Exporting the new
entry point rather than keeping it firmware-internal costs 8 bytes of API
table, and is what lets a plugin-hosting FAP fix the same hazard in its own
directory.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* SubGhz: pick the radio device plugins out of their folder by file name

subghz_device_registry_init() scans apps_data/subghz/plugins on every
subghz_devices_init(), which runs on every Sub-GHz app start and from the CLI
and JS module. Anything else that ends up in that folder - the natural place
for a Sub-GHz plugin, by fbt's own convention - was mapped, relocated and run
in full before the app id said it was not a radio driver. For the Frequency
Analyzer plugin that is 5,103 bytes of heap and ~7 KB of SD reads, every time.

Radio device plugins are all named radio_device_*.fal already, so require that
prefix and let the registry pass over the rest for the cost of a string
compare. Its "Failed to load all libs" check now means something, and says
which error it means.

The cost of keying on the file name is that a driver named anything else stops
being picked up, and a skip is only logged at debug level, which release builds
compile out. Since a missing driver otherwise shows up as nothing more than the
external module no longer being offered, warn when the scan ends with no driver
at all, and add the assertion to the Sub-GHz unit test suite - which already
inits the registry - that the one driver we ship is still found. The warning
cannot see one of several drivers going missing; the test is what covers that
if a second one is ever added.

A third-party radio driver under some other name is the one compatibility cost
here; radio_device_cc1101_ext.fal is the only driver in the tree and the only
naming an out-of-tree one could have copied.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* SubGhz: put the Frequency Analyzer plugin back in the default plugin directory

PR #1131 deployed it to apps_data/subghz/plugins/features/ so the radio device
registry's non-recursive scan would never see it, and added an fal_path field
to the app manifest to allow that. With the registry filtering by file name and
the loader skipping what it cannot load, the folder is safe to share, so the
plugin goes back to apps_data/subghz/plugins/ and fal_path goes away with it.

Nothing else used fal_path, and it has not shipped in a release, so no SDK
consumer can be relying on it; every other plugin already deploys to the
default apps_data/<parent appid>/plugins. It was also a divergence in
scripts/fbt from OFW, which is rebase surface we do not need to carry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* CHANGELOG: plugin loader scan fix, API 88.7

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 13:19:37 +03:00

108 lines
3.5 KiB
C

#pragma once
#include <flipper_application/flipper_application.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Object that manages plugins for an application
* Implements mass loading of plugins and provides access to their descriptors
*/
typedef struct PluginManager PluginManager;
typedef enum {
PluginManagerErrorNone = 0,
PluginManagerErrorLoaderError,
PluginManagerErrorApplicationIdMismatch,
PluginManagerErrorAPIVersionMismatch,
} PluginManagerError;
/**
* @brief Allocates new PluginManager
* @param application_id Application ID filter - only plugins with matching ID will be loaded
* @param api_version Application API version filter - only plugins with matching API version
* @param api_interface Application API interface - used to resolve plugins' API imports
* If plugin uses private application's API, use CompoundApiInterface
* @return new PluginManager instance
*/
PluginManager* plugin_manager_alloc(
const char* application_id,
uint32_t api_version,
const ElfApiInterface* api_interface);
/**
* @brief Frees PluginManager
* @param manager PluginManager instance
*/
void plugin_manager_free(PluginManager* manager);
/**
* @brief Loads single plugin by full path
* @param manager PluginManager instance
* @param path Path to plugin
* @return Error code
*/
PluginManagerError plugin_manager_load_single(PluginManager* manager, const char* path);
/**
* @brief Loads all plugins from specified directory
*
* A file that cannot be loaded is skipped, so it does not cost the plugins listed after it, and
* a plugin belonging to another application is passed over rather than reported as a failure.
*
* @param manager PluginManager instance
* @param path Path to directory
* @return PluginManagerErrorNone if the whole directory was read and every plugin of this
* application in it loaded - a directory that cannot be opened holds none, and so counts as
* success. Otherwise the error of one that did not; use plugin_manager_get_count() for how many
* did.
*/
PluginManagerError plugin_manager_load_all(PluginManager* manager, const char* path);
/**
* @brief Loads all plugins from specified directory whose file name starts with a prefix
*
* Use this when the directory is shared with plugins of another kind. An app id mismatch is only
* visible once the image has been mapped, relocated and its init run, all of which a file name
* check costs nothing to avoid.
*
* @param manager PluginManager instance
* @param path Path to directory
* @param fal_prefix Prefix the file name must start with, compared case-sensitively, or NULL to
* consider every *.fal
* @return As plugin_manager_load_all(), over the files whose name matches the prefix
*/
PluginManagerError plugin_manager_load_all_prefixed(
PluginManager* manager,
const char* path,
const char* fal_prefix);
/**
* @brief Returns number of loaded plugins
* @param manager PluginManager instance
* @return Number of loaded plugins
*/
uint32_t plugin_manager_get_count(PluginManager* manager);
/**
* @brief Returns plugin descriptor by index
* @param manager PluginManager instance
* @param index Plugin index
* @return Plugin descriptor
*/
const FlipperAppPluginDescriptor* plugin_manager_get(PluginManager* manager, uint32_t index);
/**
* @brief Returns plugin entry point by index
* @param manager PluginManager instance
* @param index Plugin index
* @return Plugin entry point
*/
const void* plugin_manager_get_ep(PluginManager* manager, uint32_t index);
#ifdef __cplusplus
}
#endif