CDC Badge OS
Firmware for the CDC Badge v1.0 hardware security key
Loading...
Searching...
No Matches
PluginManager.cpp
Go to the documentation of this file.
7#include "wamr_runtime/Wamr.h"
8#include "cdc_ui/I18n.h"
9#include "cdc_log.h"
10#include "cdc_views/ToastView.h"
11
12#include "freertos/FreeRTOS.h"
13#include "freertos/task.h"
14#include "freertos/semphr.h"
15#include "esp_timer.h"
16
20#include "cdc_ui/ViewStack.h"
22
23extern "C" {
24#include "bh_log.h"
25}
26
27extern "C" void plg_ble_pump(void);
28extern "C" void plg_ble_on_unload(void* plugin);
29extern "C" void plg_msg_pump(void);
30extern "C" void plg_msg_on_unload(void* plugin);
31extern "C" void plg_msg_init(void);
32extern "C" void plg_ext_feature_pump(void);
33extern "C" void plg_ext_feature_on_unload(void* plugin);
34extern "C" void plg_surface_on_unload(void* plugin);
35extern "C" void plg_net_pump(void);
36extern "C" void plg_net_on_unload(void* plugin);
37extern "C" void plg_gpio_on_unload(void* plugin);
38extern "C" void plg_http_on_unload(void* plugin);
39extern "C" void plg_socket_on_unload(void* plugin);
40
41#include <algorithm>
42#include <cstdio>
43#include <cstring>
44#include <utility>
45
46namespace cdc::plugin_manager {
47
48static const char* TAG = "PLG_MGR";
49
50namespace {
51
52constexpr uint32_t TICK_INTERVAL_MS = 50;
53constexpr uint32_t TICK_STACK_BYTES = 12288;
54constexpr UBaseType_t TICK_PRIORITY = 5;
55
56// Upper bound on background plugins force-unloaded for trapping in a single
57// dispatch pass. Any beyond this are handled on the next pass.
58constexpr size_t kMaxTrapsPerDispatch = 8;
59
60void invokeDeinit(Plugin& plugin)
61{
62 int32_t rc = 0;
63 if (!plugin.callI("plugin_deinit", {}, &rc)) {
64 LOG_W(TAG, "plugin_deinit missing for %s", plugin.id().c_str());
65 } else if (rc != 0) {
66 LOG_W(TAG, "plugin_deinit returned %ld for %s",
67 static_cast<long>(rc), plugin.id().c_str());
68 }
69}
70
71// Reflect a plugin's sleep inhibitor into the SleepManager. The id string
72// (stable for the plugin's lifetime in RAM) is used as the inhibitor reason,
73// so it must be released before the plugin is unloaded.
74void applySleepInhibitor(const Plugin& plugin, bool on)
75{
77 if (on) {
78 // Auto-acquire only for the static prevent_sleep capability. Dynamic
79 // inhibitors are acquired by the plugin via host_set_sleep_inhibit.
80 if (!plugin.manifest().capabilities.prevent_sleep) return;
81 sm.addSleepInhibitor(plugin.id().c_str());
82 } else {
83 // Always release on unload: covers both the prevent_sleep capability
84 // and any inhibitor acquired dynamically. removeSleepInhibitor is a
85 // no-op when no matching inhibitor is held.
86 sm.removeSleepInhibitor(plugin.id().c_str());
87 }
88}
89
90struct ScopedLock {
91 SemaphoreHandle_t m;
92 bool taken = false;
93 explicit ScopedLock(SemaphoreHandle_t s, uint32_t timeout_ms = 1000) : m(s) {
94 if (m) taken = xSemaphoreTakeRecursive(m, pdMS_TO_TICKS(timeout_ms)) == pdTRUE;
95 }
96 ~ScopedLock() { if (taken && m) xSemaphoreGiveRecursive(m); }
97 explicit operator bool() const { return taken; }
98};
99
100} // namespace
101
102PluginManager& PluginManager::instance() noexcept
103{
104 static PluginManager s;
105 return s;
106}
107
108PluginManager::PluginManager() = default;
109PluginManager::~PluginManager() = default;
110
112{
113 if (initialised_) return true;
114
115 bh_log_set_verbose_level(BH_LOG_LEVEL_FATAL);
116
117 if (!cdc::wamr::init()) { LOG_E(TAG, "WAMR init failed"); return false; }
118 if (!PluginStorage::mount()) { LOG_E(TAG, "FAT mount failed"); return false; }
119 if (!register_host_imports()) { LOG_E(TAG, "imports failed"); return false; }
120
121 call_mutex_ = xSemaphoreCreateRecursiveMutex();
122 if (!call_mutex_) { LOG_E(TAG, "plugin mutex create failed"); return false; }
123
124 auto ids = PluginStorage::listPluginIds();
125 LOG_I(TAG, "PluginManager ready: %u plugin(s) installed",
126 static_cast<unsigned>(ids.size()));
127
128 if (!msg_index_mutex_) msg_index_mutex_ = xSemaphoreCreateMutex();
129 rebuildMessageIndex();
130 plg_msg_init(); // wire the deferred message-handler resolver into cdc_msg
131
132 initialised_ = true;
133 startTickTask();
134 loadAutoloadPlugins();
135 return true;
136}
137
139{
140 stopTickTask();
142
143 {
144 ScopedLock l(static_cast<SemaphoreHandle_t>(call_mutex_));
145 for (auto& p : background_) {
146 (void)p->callI("plugin_on_exit");
147 teardownPlugin(*p, /*runWasmDeinit=*/true);
148 }
149 background_.clear();
150 }
151
152 if (call_mutex_) {
153 vSemaphoreDelete(static_cast<SemaphoreHandle_t>(call_mutex_));
154 call_mutex_ = nullptr;
155 }
158 cdc::wamr::deinit();
159 initialised_ = false;
160}
161
162std::vector<std::string> PluginManager::listInstalledIds() const
163{
165}
166
167std::optional<PluginManifest>
168PluginManager::getManifest(const std::string& id) const
169{
170 std::string meta = PluginStorage::metaPath(id);
171 auto fp = ::cdc::core::openFile(meta.c_str(), "rb");
172 if (!fp) return std::nullopt;
173 std::fseek(fp.get(), 0, SEEK_END);
174 long n = std::ftell(fp.get());
175 if (n <= 0) return std::nullopt;
176 std::fseek(fp.get(), 0, SEEK_SET);
177 std::string buf(static_cast<size_t>(n), '\0');
178 if (std::fread(buf.data(), 1, n, fp.get()) != static_cast<size_t>(n)) {
179 return std::nullopt;
180 }
181 PluginManifest out;
182 if (!PluginManifest::parse(buf.data(), buf.size(), out)) return std::nullopt;
183 return out;
184}
185
186bool PluginManager::isPluginDisabled(const std::string& id) const
187{
188 return PluginStorage::isDisabled(id);
189}
190
191bool PluginManager::setPluginDisabled(const std::string& id, bool disabled)
192{
193 if (!getManifest(id)) return false;
194 if (!PluginStorage::setDisabled(id, disabled)) return false;
195 if (disabled) {
196 (void)unloadFromRam(id);
197 }
198 return true;
199}
200
201StartResult PluginManager::startPlugin(const std::string& id_ref)
202{
203 ScopedLock lock(static_cast<SemaphoreHandle_t>(call_mutex_));
204 if (!lock) return StartResult::Busy;
205 const std::string id = id_ref;
206
207 if (isPluginDisabled(id)) {
208 LOG_W(TAG, "plugin %s is disabled", id.c_str());
210 }
211
212 if (active_ && active_->id() == id) {
213 (void)active_->callI("plugin_on_enter");
214 // Discard a stop queued by the modal that was dismissed to launch this.
215 pending_stop_.store(false, std::memory_order_release);
216 return StartResult::Ok;
217 }
218
219 if (active_) {
220 (void)active_->callI("plugin_on_exit");
221 if (active_->manifest().capabilities.background && active_->residentRequested()) {
222 background_.push_back(std::move(active_));
223 } else {
224 teardownPlugin(*active_, /*runWasmDeinit=*/true);
225 }
226 active_.reset();
227 }
228
229 // If the plugin is already running in the background, promote it
230 // to foreground without reloading.
231 auto it = std::find_if(background_.begin(), background_.end(),
232 [&](const std::unique_ptr<Plugin>& p) {
233 return p && p->id() == id;
234 });
235 if (it != background_.end()) {
236 auto plugin = std::move(*it);
237 background_.erase(it);
238
239 plugin_base_depth_ = cdc::ui::ViewStack::instance().depth();
240 int32_t enter_rc = 0;
241 if (!plugin->callI("plugin_on_enter", {}, &enter_rc)) {
242 LOG_E(TAG, "plugin_on_enter missing for %s", id.c_str());
243 background_.push_back(std::move(plugin)); // keep it as background
245 }
246 active_ = std::move(plugin);
247 // Discard a stop queued by the modal that was dismissed to launch this.
248 pending_stop_.store(false, std::memory_order_release);
249 LOG_I(TAG, "plugin %s promoted bg->fg", id.c_str());
250 return StartResult::Ok;
251 }
252
253 auto mf_opt = getManifest(id);
254 if (!mf_opt) {
255 LOG_W(TAG, "manifest missing/invalid for %s", id.c_str());
257 }
258 const PluginManifest& mf = *mf_opt;
259
260 auto check = CapabilityChecker::validate(mf);
261 if (!check.ok()) {
262 LOG_W(TAG, "capability check failed for %s: %s",
263 id.c_str(), check.detail.c_str());
265 }
266
267 auto plugin = std::make_unique<Plugin>();
268 if (!plugin->load(id, mf)) {
269 LOG_E(TAG, "Plugin::load failed for %s", id.c_str());
271 }
272 plugin->loadLangOverlay();
273
274 int32_t init_rc = 0;
275 if (!plugin->callI("plugin_init", {}, &init_rc) || init_rc != 0) {
276 LOG_E(TAG, "plugin_init failed for %s (rc=%ld)", id.c_str(),
277 static_cast<long>(init_rc));
278 teardownPlugin(*plugin, /*runWasmDeinit=*/!plugin->lastCallTrapped());
280 }
281
282 std::string failed_name, on_fail;
283 PrereqResult pr = Prerequisites::walk(*plugin, failed_name, on_fail);
284 if (pr == PrereqResult::HardFailed) {
285 LOG_E(TAG, "prerequisite '%s' aborted start of %s",
286 failed_name.c_str(), id.c_str());
287 teardownPlugin(*plugin, /*runWasmDeinit=*/true);
289 }
290 if (pr == PrereqResult::SoftFailed) {
291 LOG_W(TAG, "prerequisite '%s' soft-failed for %s, continuing",
292 failed_name.c_str(), id.c_str());
293 }
294
295 static cdc::ui::ToastView s_loading_toast;
296 char loading_msg[80];
297 {
298 const auto& meta = plugin->manifest().i18n_meta;
299 const char* display = id.c_str();
300 auto it = meta.find("name");
301 if (it != meta.end() && !it->second.by_lang.empty()) {
302 display = it->second.by_lang.begin()->second.c_str();
303 }
304 std::snprintf(loading_msg, sizeof(loading_msg), "%s\n%s",
305 cdc::ui::tr("core.plugin_loading"), display);
306 }
307 s_loading_toast.init(loading_msg, cdc::ui::ToastView::Icon::TASK, 0, true);
308 cdc::ui::ViewStack::instance().showModal(&s_loading_toast);
309 cdc::ui::ViewStack::instance().render(true); // paint loading toast before the blocking plugin_on_enter
310
311 auto hide_loading_if_top = []() {
312 auto& vs = cdc::ui::ViewStack::instance();
313 if (vs.getModal() == &s_loading_toast) {
314 vs.hideModal();
315 }
316 };
317
318 plugin_base_depth_ = cdc::ui::ViewStack::instance().depth();
319 int32_t enter_rc = 0;
320 if (!plugin->callI("plugin_on_enter", {}, &enter_rc)) {
321 hide_loading_if_top();
322 if (plugin->lastCallTrapped()) {
323 LOG_E(TAG, "plugin_on_enter trapped for %s: %s", id.c_str(),
324 plugin->lastTrapMessage());
325 } else {
326 LOG_E(TAG, "plugin_on_enter export missing for %s", id.c_str());
327 }
328 teardownPlugin(*plugin, /*runWasmDeinit=*/!plugin->lastCallTrapped());
330 }
331 hide_loading_if_top();
332 if (enter_rc != 0) {
333 LOG_W(TAG, "plugin_on_enter returned %ld for %s",
334 static_cast<long>(enter_rc), id.c_str());
335 }
336
337 active_ = std::move(plugin);
338 // Discard a stop queued by the modal that was dismissed to launch this.
339 pending_stop_.store(false, std::memory_order_release);
340 applySleepInhibitor(*active_, true);
341 LOG_I(TAG, "plugin %s started", id.c_str());
342 return StartResult::Ok;
343}
344
346{
347 pending_stop_.store(true, std::memory_order_release);
348}
349
351{
352 ScopedLock lock(static_cast<SemaphoreHandle_t>(call_mutex_));
353 if (!lock) return false;
354 if (!active_) return false;
355 LOG_I(TAG, "stopping plugin %s", active_->id().c_str());
356 (void)active_->callI("plugin_on_exit");
357
358 // Pop the plugin's foreground views and reset its UI state before changing
359 // residency or tearing the instance down, so no live view can reference a
360 // freed/demoted plugin (mirrors unloadFromRam). The GUI stop path already
361 // runs with the views popped; the serial STOP path does not.
362 while (cdc::ui::ViewStack::instance().depth() > 1) {
364 }
366
367 // If the plugin is a background service AND asked to stay resident, demote
368 // it back instead of unloading. Capability alone is only permission.
369 if (active_->manifest().capabilities.background && active_->residentRequested()) {
370 background_.push_back(std::move(active_));
371 active_.reset();
372 return true;
373 }
374
375 teardownPlugin(*active_, /*runWasmDeinit=*/true);
376 active_.reset();
377 return true;
378}
379
380bool PluginManager::unloadFromRam(const std::string& id)
381{
382 ScopedLock lock(static_cast<SemaphoreHandle_t>(call_mutex_));
383 if (!lock) return false;
384 if (active_ && active_->id() == id) {
385 LOG_I(TAG, "unloading active plugin %s from RAM", id.c_str());
386 (void)active_->callI("plugin_on_exit");
387 while (cdc::ui::ViewStack::instance().depth() > 1) {
389 }
391 teardownPlugin(*active_, /*runWasmDeinit=*/true);
392 active_.reset();
393 return true;
394 }
395 auto it = std::find_if(background_.begin(), background_.end(),
396 [&](const std::unique_ptr<Plugin>& p) {
397 return p && p->id() == id;
398 });
399 if (it == background_.end()) return false;
400 LOG_I(TAG, "unloading background plugin %s from RAM", id.c_str());
401 teardownPlugin(**it, /*runWasmDeinit=*/true);
402 background_.erase(it);
403 return true;
404}
405
406bool PluginManager::uninstallPlugin(const std::string& id)
407{
408 (void)unloadFromRam(id);
409 std::remove(PluginStorage::wasmPath(id).c_str());
410 std::remove(PluginStorage::aotPath(id).c_str());
411 std::remove(PluginStorage::metaPath(id).c_str());
412 std::remove(PluginStorage::langPath(id).c_str());
413 std::remove(PluginStorage::disabledPath(id).c_str());
414 LOG_I(TAG, "uninstalled plugin %s", id.c_str());
415 return true;
416}
417
419{
420 ScopedLock lock(static_cast<SemaphoreHandle_t>(call_mutex_));
421 if (!lock) return;
422 if (active_) {
423 LOG_I(TAG, "unloading active plugin %s from RAM", active_->id().c_str());
424 (void)active_->callI("plugin_on_exit");
425 while (cdc::ui::ViewStack::instance().depth() > 1) {
427 }
429 teardownPlugin(*active_, /*runWasmDeinit=*/true);
430 active_.reset();
431 }
432 for (auto& p : background_) {
433 if (p) {
434 LOG_I(TAG, "unloading background plugin %s from RAM", p->id().c_str());
435 teardownPlugin(*p, /*runWasmDeinit=*/true);
436 }
437 }
438 background_.clear();
439}
440
441void PluginManager::teardownPlugin(Plugin& p, bool runWasmDeinit)
442{
443 if (runWasmDeinit) invokeDeinit(p);
444 applySleepInhibitor(p, false);
455 p.unload();
456}
457
458void PluginManager::handleTrap(Plugin& p, const char* fn)
459{
460 LOG_E(TAG, "==================== PLUGIN TRAP ====================");
461 LOG_E(TAG, "plugin '%s' trapped in %s", p.id().c_str(), fn);
462 LOG_E(TAG, " reason : %s", p.lastTrapMessage());
463 LOG_E(TAG, " action : force-unload + release all held resources");
464
465 if (&p == active_.get()) {
466 auto& vs = cdc::ui::ViewStack::instance();
467 while (vs.depth() > plugin_base_depth_ && vs.depth() > 1) vs.pop();
469 teardownPlugin(p, /*runWasmDeinit=*/false);
470 active_.reset();
471 LOG_E(TAG, "=====================================================");
472 return;
473 }
474 for (auto it = background_.begin(); it != background_.end(); ++it) {
475 if (it->get() == &p) {
476 teardownPlugin(**it, /*runWasmDeinit=*/false);
477 background_.erase(it);
478 LOG_E(TAG, "=====================================================");
479 return;
480 }
481 }
482 LOG_E(TAG, "=====================================================");
483}
484
485bool PluginManager::reloadBackgroundPlugin(const std::string& id)
486{
487 if (isPluginDisabled(id)) return false;
488
489 auto manifest = getManifest(id);
490 if (!manifest || !manifest->capabilities.background) return false;
491
492 // background:true is not "start at boot": only refresh an instance that is
493 // already running in the background so it picks up a freshly uploaded
494 // binary. An idle plugin stays unloaded until the user starts it manually.
495 if (!isRunningInBackground(id)) return false;
496
497 (void)unloadFromRam(id);
498
499 ScopedLock lock(static_cast<SemaphoreHandle_t>(call_mutex_));
500 if (!lock) return false;
501 if (!loadIntoBackground(id, *manifest)) return false;
502 LOG_I(TAG, "background plugin %s reloaded", id.c_str());
503 return true;
504}
505
506bool PluginManager::loadIntoBackground(const std::string& id, const PluginManifest& mf)
507{
508 if (isPluginDisabled(id)) {
509 LOG_I(TAG, "bg load %s skipped: disabled", id.c_str());
510 return false;
511 }
512
513 auto plugin = std::make_unique<Plugin>();
514 if (!plugin->load(id, mf)) {
515 LOG_E(TAG, "bg load %s: load failed", id.c_str());
516 return false;
517 }
518 plugin->loadLangOverlay();
519 int32_t init_rc = 0;
520 if (!plugin->callI("plugin_init", {}, &init_rc) || init_rc != 0) {
521 LOG_E(TAG, "bg load %s: plugin_init failed (rc=%ld)", id.c_str(),
522 static_cast<long>(init_rc));
523 teardownPlugin(*plugin, /*runWasmDeinit=*/!plugin->lastCallTrapped());
524 return false;
525 }
526 std::string failed_name, on_fail;
527 PrereqResult pr = Prerequisites::walk(*plugin, failed_name, on_fail);
528 if (pr == PrereqResult::HardFailed) {
529 LOG_E(TAG, "bg load %s: prereq '%s' aborted", id.c_str(), failed_name.c_str());
530 teardownPlugin(*plugin, /*runWasmDeinit=*/true);
531 return false;
532 }
533 background_.push_back(std::move(plugin));
534 applySleepInhibitor(*background_.back(), true);
535 return true;
536}
537
538void PluginManager::loadAutoloadPlugins()
539{
540 auto ids = PluginStorage::listPluginIds();
541 ScopedLock lock(static_cast<SemaphoreHandle_t>(call_mutex_));
542 if (!lock) return;
543 for (const auto& id : ids) {
544 auto mf = getManifest(id);
545 if (!mf || !mf->capabilities.autoload) continue;
546 if (isPluginDisabled(id)) {
547 LOG_I(TAG, "autoload %s skipped: disabled", id.c_str());
548 continue;
549 }
550 if (isLoaded(id)) continue;
551
552 auto check = CapabilityChecker::validate(*mf);
553 if (!check.ok()) {
554 LOG_W(TAG, "autoload %s rejected: %s", id.c_str(), check.detail.c_str());
555 continue;
556 }
557 if (loadIntoBackground(id, *mf)) {
558 // Autoload is opt-in: plugin_init ran (its chance to call
559 // host_set_resident(true)); keep it resident only if it did.
560 // Autoload residency does NOT require the `background` capability -
561 // an autoload plugin is a headless boot resident by definition,
562 // unlike the foreground-exit demotion path (stopActivePlugin),
563 // which needs `background && residentRequested()`.
564 if (!background_.empty() && background_.back()->residentRequested()) {
565 LOG_I(TAG, "autoloaded plugin %s (resident)", id.c_str());
566 } else {
567 LOG_I(TAG, "autoload %s: no set_resident(true), not staying resident",
568 id.c_str());
569 unloadFromRam(id);
570 }
571 } else {
572 LOG_W(TAG, "autoload of %s failed", id.c_str());
573 }
574 }
575}
576
577void PluginManager::rebuildMessageIndex()
578{
579 std::vector<std::string> mimes, mids;
580 std::vector<std::string> feats, fids;
581 for (const auto& id : PluginStorage::listPluginIds()) {
582 if (isPluginDisabled(id)) continue; // a disabled plugin can't be activated
583 auto mf = getManifest(id);
584 if (!mf) continue;
585 for (const auto& mt : mf->capabilities.message_types) {
586 mimes.push_back(mt);
587 mids.push_back(id);
588 }
589 for (const auto& feat : mf->capabilities.provides) {
590 bool duplicate = false;
591 for (size_t i = 0; i < feats.size(); ++i) {
592 if (feats[i] == feat) { duplicate = true;
593 LOG_W(TAG, "feature '%s' already provided by %s, ignoring %s",
594 feat.c_str(), fids[i].c_str(), id.c_str());
595 break;
596 }
597 }
598 if (duplicate) continue;
599 feats.push_back(feat);
600 fids.push_back(id);
601 }
602 }
603 auto* m = static_cast<SemaphoreHandle_t>(msg_index_mutex_);
604 if (m) xSemaphoreTake(m, portMAX_DELAY);
605 msg_index_mime_.swap(mimes);
606 msg_index_id_.swap(mids);
607 feat_index_name_.swap(feats);
608 feat_index_id_.swap(fids);
609 if (m) xSemaphoreGive(m);
610}
611
612void PluginManager::maybeRefreshMessageIndex()
613{
614 uint32_t now = static_cast<uint32_t>(esp_timer_get_time() / 1000);
615 if (now - last_index_refresh_ms_ < 2000) return;
616 last_index_refresh_ms_ = now;
617 std::string sig;
618 for (const auto& id : PluginStorage::listPluginIds()) { sig += id; sig.push_back(','); }
619 if (sig == installed_sig_) return;
620 installed_sig_ = sig;
621 rebuildMessageIndex();
622}
623
624bool PluginManager::messageTypeInstalled(const char* mime) const
625{
626 if (!mime) return false;
627 auto* m = static_cast<SemaphoreHandle_t>(msg_index_mutex_);
628 // Called from the BLE host task: never wait indefinitely. On contention,
629 // fail closed (treat as not-installed -> the offer is auto-declined).
630 if (m && xSemaphoreTake(m, pdMS_TO_TICKS(20)) != pdTRUE) return false;
631 bool found = false;
632 for (const auto& mt : msg_index_mime_) {
633 if (mt == mime) { found = true; break; }
634 }
635 if (m) xSemaphoreGive(m);
636 return found;
637}
638
639bool PluginManager::featureInstalled(const char* feature) const
640{
641 if (!feature) return false;
642 auto* m = static_cast<SemaphoreHandle_t>(msg_index_mutex_);
643 if (m && xSemaphoreTake(m, pdMS_TO_TICKS(20)) != pdTRUE) return false;
644 bool found = false;
645 for (const auto& name : feat_index_name_) {
646 if (name == feature) { found = true; break; }
647 }
648 if (m) xSemaphoreGive(m);
649 return found;
650}
651
652std::string PluginManager::featureProviderId(const char* feature) const
653{
654 if (!feature) return {};
655 std::string id;
656 auto* m = static_cast<SemaphoreHandle_t>(msg_index_mutex_);
657 // Same bounded, fail-closed policy as featureInstalled(): the two run
658 // back-to-back on the same path, so both must use the 20 ms timeout or a
659 // caller could see "available" then "no provider". On contention return
660 // no provider (empty id), matching the not-found result.
661 if (m && xSemaphoreTake(m, pdMS_TO_TICKS(20)) != pdTRUE) return {};
662 for (size_t i = 0; i < feat_index_name_.size(); ++i) {
663 if (feat_index_name_[i] == feature) { id = feat_index_id_[i]; break; }
664 }
665 if (m) xSemaphoreGive(m);
666 return id;
667}
668
670{
671 if (!mime) return false;
672 std::string id;
673 {
674 auto* m = static_cast<SemaphoreHandle_t>(msg_index_mutex_);
675 if (m) xSemaphoreTake(m, portMAX_DELAY);
676 for (size_t i = 0; i < msg_index_mime_.size(); ++i) {
677 if (msg_index_mime_[i] == mime) { id = msg_index_id_[i]; break; }
678 }
679 if (m) xSemaphoreGive(m);
680 }
681 if (id.empty()) return false;
682
683 ScopedLock lock(static_cast<SemaphoreHandle_t>(call_mutex_));
684 if (!lock) return false;
685 if (isLoaded(id)) return true; // already running: its handler is live
686 if (isPluginDisabled(id)) return false;
687 auto mf = getManifest(id);
688 if (!mf) return false;
689 auto check = CapabilityChecker::validate(*mf);
690 if (!check.ok()) {
691 LOG_W(TAG, "msg activate %s rejected: %s", id.c_str(), check.detail.c_str());
692 return false;
693 }
694 return loadIntoBackground(id, *mf);
695}
696
697uint8_t PluginManager::getLockscreenItems(LockscreenItem* out, uint8_t max) const
698{
700 const uint8_t n = collectLockscreenItems(regs, sizeof(regs) / sizeof(regs[0]));
701 const uint8_t copy = n < max ? n : max;
702 const std::string lang = ::cdc::ui::I18n::instance().getLanguageCode();
703
704 for (uint8_t i = 0; i < copy; ++i) {
705 Plugin* p = static_cast<Plugin*>(regs[i].plugin);
706 out[i].plugin = p;
707 out[i].action_id = regs[i].action_id;
708
709 const char* label = nullptr;
710 if (p) {
711 label = p->trKey(regs[i].label_key); // lang overlay
712 if (!label) {
713 // Fallback: manifest i18n.strings.<key>.<lang|en|first>
714 const auto& strings = p->manifest().i18n_strings;
715 auto it = strings.find(regs[i].label_key);
716 if (it != strings.end()) {
717 auto pick = [&](const std::string& l) -> const char* {
718 auto lit = it->second.by_lang.find(l);
719 return lit != it->second.by_lang.end() ? lit->second.c_str() : nullptr;
720 };
721 label = pick(lang);
722 if (!label) label = pick("en");
723 if (!label && !it->second.by_lang.empty())
724 label = it->second.by_lang.begin()->second.c_str();
725 }
726 }
727 }
728 if (!label) label = regs[i].label_key;
729 std::strncpy(out[i].label, label, sizeof(out[i].label) - 1);
730 out[i].label[sizeof(out[i].label) - 1] = '\0';
731 }
732 return copy;
733}
734
739
740bool PluginManager::hasActivePlugin() const noexcept { return active_ != nullptr; }
741std::string PluginManager::activePluginId() const { return active_ ? active_->id() : std::string{}; }
742
743bool PluginManager::isLoaded(const std::string& id) const
744{
745 if (active_ && active_->id() == id) return true;
746 for (const auto& p : background_) if (p->id() == id) return true;
747 return false;
748}
749
750bool PluginManager::isRunningInBackground(const std::string& id) const
751{
752 for (const auto& p : background_) if (p->id() == id) return true;
753 return false;
754}
755
757{
758 return active_ && active_->manifest().capabilities.background;
759}
760
762{
763 return active_ && active_->manifest().capabilities.prevent_sleep;
764}
765
767{
768 return !background_.empty();
769}
770
771void PluginManager::dispatchButton(uint32_t button_code)
772{
773 ScopedLock lock(static_cast<SemaphoreHandle_t>(call_mutex_));
774 if (!lock) return;
775 if (!active_) return;
776 int32_t rc = 0;
777 (void)active_->callI("plugin_on_button",
778 {static_cast<int32_t>(button_code)}, &rc);
779 if (active_->lastCallTrapped()) handleTrap(*active_, "plugin_on_button");
780}
781
782void PluginManager::dispatchAction(uint32_t action_id, uint32_t idx, uint32_t user_data)
783{
784 ScopedLock lock(static_cast<SemaphoreHandle_t>(call_mutex_));
785 if (!lock) return;
786 if (!active_) return;
787 int32_t rc = 0;
788 (void)active_->callI("plugin_on_action",
789 {static_cast<int32_t>(action_id),
790 static_cast<int32_t>(idx),
791 static_cast<int32_t>(user_data)}, &rc);
792 if (active_->lastCallTrapped()) handleTrap(*active_, "plugin_on_action");
793}
794
795void PluginManager::dispatchActionTo(Plugin* plugin, uint32_t action_id,
796 uint32_t idx, uint32_t user_data)
797{
798 if (!plugin) return;
799 ScopedLock lock(static_cast<SemaphoreHandle_t>(call_mutex_));
800 if (!lock) return;
801
802 bool found = (active_.get() == plugin);
803 if (!found) {
804 for (auto& p : background_) if (p.get() == plugin) { found = true; break; }
805 }
806 if (!found) return; // stale subscription, plugin no longer loaded
807
808 int32_t rc = 0;
809 (void)plugin->callI("plugin_on_action",
810 {static_cast<int32_t>(action_id),
811 static_cast<int32_t>(idx),
812 static_cast<int32_t>(user_data)}, &rc);
813 if (plugin->lastCallTrapped()) handleTrap(*plugin, "plugin_on_action");
814}
815
816void PluginManager::dispatchTick(uint64_t uptime_ms)
817{
818 ScopedLock lock(static_cast<SemaphoreHandle_t>(call_mutex_));
819 if (!lock) return;
820 int32_t rc = 0;
821 const int32_t hi = static_cast<int32_t>(uptime_ms >> 32);
822 const int32_t lo = static_cast<int32_t>(uptime_ms & 0xFFFFFFFFu);
823 if (active_) {
824 (void)active_->callI("plugin_on_tick", {lo, hi}, &rc);
825 if (active_->lastCallTrapped()) handleTrap(*active_, "plugin_on_tick");
826 }
827 Plugin* trapped[kMaxTrapsPerDispatch];
828 size_t n_trapped = 0;
829 for (auto& p : background_) {
830 (void)p->callI("plugin_on_tick", {lo, hi}, &rc);
831 if (p->lastCallTrapped() && n_trapped < kMaxTrapsPerDispatch)
832 trapped[n_trapped++] = p.get();
833 }
834 for (size_t i = 0; i < n_trapped; ++i) handleTrap(*trapped[i], "plugin_on_tick");
835 plg_ble_pump();
836 plg_msg_pump();
838 plg_net_pump();
839}
840
841void PluginManager::dispatchEventAll(uint32_t event_type, uint32_t value)
842{
843 ScopedLock lock(static_cast<SemaphoreHandle_t>(call_mutex_));
844 if (!lock) return;
845 int32_t rc = 0;
846 if (active_) {
847 (void)active_->callI("plugin_on_event",
848 {static_cast<int32_t>(event_type),
849 static_cast<int32_t>(value)}, &rc);
850 if (active_->lastCallTrapped()) handleTrap(*active_, "plugin_on_event");
851 }
852 Plugin* trapped[kMaxTrapsPerDispatch];
853 size_t n_trapped = 0;
854 for (auto& p : background_) {
855 (void)p->callI("plugin_on_event",
856 {static_cast<int32_t>(event_type),
857 static_cast<int32_t>(value)}, &rc);
858 if (p->lastCallTrapped() && n_trapped < kMaxTrapsPerDispatch)
859 trapped[n_trapped++] = p.get();
860 }
861 for (size_t i = 0; i < n_trapped; ++i) handleTrap(*trapped[i], "plugin_on_event");
862}
863
864bool PluginManager::dispatchCmd(const std::string& id, const char* cmd, size_t len)
865{
866 ScopedLock lock(static_cast<SemaphoreHandle_t>(call_mutex_));
867 if (!lock) return false;
868
869 Plugin* target = (active_ && active_->id() == id) ? active_.get() : nullptr;
870 if (!target) {
871 for (auto& p : background_) if (p->id() == id) { target = p.get(); break; }
872 }
873 if (!target || !target->hasExport("plugin_on_cmd")) return false;
874
875 pending_cmd_.assign(cmd ? cmd : "", len);
876 int32_t rc = 0;
877 (void)target->callI("plugin_on_cmd", {static_cast<int32_t>(len)}, &rc);
878 pending_cmd_.clear();
879 if (target->lastCallTrapped()) handleTrap(*target, "plugin_on_cmd");
880 return true;
881}
882
883int PluginManager::consumeCmd(char* out, size_t out_size)
884{
885 if (!out || out_size == 0) return HOST_ERR_INVALID_ARG;
886 size_t n = pending_cmd_.size();
887 if (n >= out_size) n = out_size - 1;
888 std::memcpy(out, pending_cmd_.data(), n);
889 out[n] = '\0';
890 pending_cmd_.clear();
891 return static_cast<int>(n);
892}
893
894void PluginManager::forEachPlugin(const std::function<bool(Plugin&)>& visitor)
895{
896 ScopedLock lock(static_cast<SemaphoreHandle_t>(call_mutex_));
897 if (!lock) return;
898 if (active_) { if (!visitor(*active_)) return; }
899 for (auto& p : background_) {
900 if (!visitor(*p)) return;
901 }
902}
903
905{
906 ScopedLock lock(static_cast<SemaphoreHandle_t>(call_mutex_));
907 if (!lock) return;
908 if (active_) active_->loadLangOverlay();
909 for (auto& p : background_) p->loadLangOverlay();
910}
911
912void PluginManager::tickTaskTrampoline(void* arg)
913{
914 static_cast<PluginManager*>(arg)->tickTaskLoop();
915 vTaskDelete(nullptr);
916}
917
918void PluginManager::tickTaskLoop()
919{
920 TickType_t last = xTaskGetTickCount();
921 while (!tick_stop_) {
922 vTaskDelayUntil(&last, pdMS_TO_TICKS(TICK_INTERVAL_MS));
923 if (tick_stop_) break;
924 maybeRefreshMessageIndex();
925 if (pending_stop_.exchange(false, std::memory_order_acq_rel)) {
927 PluginListView* listView = PluginListView::active();
928 if (top && listView && top == static_cast<cdc::ui::IView*>(static_cast<cdc::ui::ViewBase*>(listView))) {
930 }
931 }
932 dispatchTick(esp_timer_get_time() / 1000);
933 }
934}
935
936void PluginManager::startTickTask()
937{
938 if (tick_task_) return;
939 tick_stop_ = false;
940 TaskHandle_t handle = nullptr;
941 if (xTaskCreate(&PluginManager::tickTaskTrampoline,
942 "plg_tick", TICK_STACK_BYTES, this,
943 TICK_PRIORITY, &handle) != pdPASS) {
944 LOG_E(TAG, "failed to spawn plg_tick task");
945 return;
946 }
947 tick_task_ = handle;
948}
949
950void PluginManager::stopTickTask()
951{
952 if (!tick_task_) return;
953 tick_stop_ = true;
954 // The task self-deletes after its next wake-up.
955 // Wait briefly so it doesn't reference us after we go away.
956 vTaskDelay(pdMS_TO_TICKS(TICK_INTERVAL_MS * 2));
957 tick_task_ = nullptr;
958}
959
960} // namespace cdc::plugin_manager
static const char * TAG
Load-time validation of plugin capabilities + manifest sanity.
Internationalization with English fallbacks in code and overlay translations loaded at runtime from a...
Internal registry of plugin lockscreen quick-actions.
Main-menu entry "Plugins" - lists all installed WASM plugins.
void plg_ext_feature_pump(void)
void plg_msg_pump(void)
void plg_ble_on_unload(void *plugin)
void plg_msg_on_unload(void *plugin)
void plg_net_pump(void)
void plg_ble_pump(void)
void plg_msg_init(void)
void plg_http_on_unload(void *plugin)
void plg_socket_on_unload(void *plugin)
void plg_gpio_on_unload(void *plugin)
void plg_surface_on_unload(void *plugin)
void plg_ext_feature_on_unload(void *plugin)
void plg_net_on_unload(void *plugin)
Discovers, loads, runs and unloads WASM plugins on the badge.
Singleton that owns plugin-pushed UI views (lists, confirms, inputs).
Owned WAMR module instance + per-plugin state.
Walks the prerequisites list of a plugin manifest before plugin_on_enter.
char name[cdc::hal::ISecureElement::RMEM_NAME_LEN]
Registers the host API as WAMR native imports under module "cdc".
CDC Log: logging over TinyUSB CDC and UART.
#define LOG_W(tag, fmt,...)
Definition cdc_log.h:146
#define LOG_I(tag, fmt,...)
Definition cdc_log.h:147
#define LOG_E(tag, fmt,...)
Definition cdc_log.h:145
static CapabilityCheckResult validate(const PluginManifest &manifest)
static PluginListView * active() noexcept
Currently-mounted PluginListView instance, or nullptr if none.
bool dispatchCmd(const std::string &id, const char *cmd, size_t len)
bool activateForMessageType(const char *mime)
Load + start (headless) the installed plugin that declares this MIME type, so its message handler bec...
uint8_t getLockscreenItems(LockscreenItem *out, uint8_t max) const
Snapshot of all plugin lockscreen items. Returns the number written.
void dispatchAction(uint32_t action_id, uint32_t idx, uint32_t user_data)
int consumeCmd(char *out, size_t out_size)
bool hasBackgroundPlugin() const noexcept
True if at least one plugin is currently resident in the background slot.
bool isLoaded(const std::string &id) const
True if a plugin with id is loaded in RAM (foreground or background).
void dispatchButton(uint32_t button_code)
bool hasActivePlugin() const noexcept
bool isRunningInBackground(const std::string &id) const
True if a plugin with id is currently resident in the background slot.
void forEachPlugin(const std::function< bool(Plugin &)> &visitor)
Iterate foreground + background plugins. Visitor returns false to stop.
std::vector< std::string > listInstalledIds() const
static PluginManager & instance() noexcept
bool unloadFromRam(const std::string &id)
void dispatchActionTo(Plugin *plugin, uint32_t action_id, uint32_t idx, uint32_t user_data)
bool setPluginDisabled(const std::string &id, bool disabled)
void dispatchTick(uint64_t uptime_ms)
bool uninstallPlugin(const std::string &id)
void triggerLockscreenItem(const LockscreenItem &item)
Fire plugin_on_action(item.action_id, 0, 0) on the owning plugin.
bool reloadBackgroundPlugin(const std::string &id)
bool featureInstalled(const char *feature) const
True if any installed plugin's manifest provides this external feature. Reads the cached index (same ...
bool messageTypeInstalled(const char *mime) const
True if any installed plugin's manifest declares this MIME type for message transfer....
bool isPluginDisabled(const std::string &id) const
std::string featureProviderId(const char *feature) const
Installed plugin id providing this external feature, or empty.
std::optional< PluginManifest > getManifest(const std::string &id) const
void dispatchEventAll(uint32_t event_type, uint32_t value)
StartResult startPlugin(const std::string &id)
static bool mount()
Mount the plugins partition. Auto-formats if empty.
static std::string langPath(const std::string &id)
Returns the full VFS path of <id>.lang (translation overlay).
static void unmount()
Unmount the plugins partition (rarely used; mostly tests).
static std::string aotPath(const std::string &id)
Returns the full VFS path of <id>.aot.
static bool isDisabled(const std::string &id)
True when the plugin has a persistent disabled marker.
static bool setDisabled(const std::string &id, bool disabled)
Create or remove the persistent disabled marker for a plugin.
static std::string disabledPath(const std::string &id)
Returns the full VFS path of <id>.disabled.
static std::vector< std::string > listPluginIds()
Discover all installed plugin ids. A plugin is recognised by the presence of both <id>....
static std::string wasmPath(const std::string &id)
Returns the full VFS path of <id>.wasm.
static std::string metaPath(const std::string &id)
Returns the full VFS path of <id>.meta.
static PluginUiState & instance() noexcept
bool hasExport(const char *name) const
Definition Plugin.cpp:128
const char * trKey(const char *key) const noexcept
Look up a plugin-local translation key in the loaded overlay.
Definition Plugin.cpp:260
bool lastCallTrapped() const noexcept
Definition Plugin.h:82
bool callI(const char *name, std::initializer_list< int32_t > args={}, int32_t *out_i32=nullptr)
Call an exported i32(i32...)->i32 function by name.
Definition Plugin.cpp:145
void unload() noexcept
Destroy WAMR instance + free bytecode buffer. Idempotent.
Definition Plugin.cpp:134
const std::string & id() const noexcept
Definition Plugin.h:89
const PluginManifest & manifest() const noexcept
Definition Plugin.h:88
static PrereqResult walk(Plugin &plugin, std::string &out_failed_name, std::string &out_on_fail)
Walk the plugin's prerequisite list in order. Marks acquired resources on the Plugin so release() can...
static void release(Plugin &plugin)
Release every resource the plugin acquired during walk(), in reverse order of acquisition.
const std::string & getLanguageCode() const
Current language code (lower-case ISO-639-1, e.g. "en", "de").
Definition I18n.h:131
static I18n & instance()
Singleton accessor.
Definition I18n.cpp:306
static SleepManager & instance()
Returns singleton sleep manager instance.
void init(const char *message, Icon icon=Icon::NONE, uint16_t durationMs=1500, bool dismissible=true)
Initializes toast message content and timing behavior.
Definition ToastView.cpp:26
IView * current() const
void render(bool synchronous=false)
Render current view (and modal if present) and flush to display.
static ViewStack & instance()
Returns singleton view-stack instance.
Definition ViewStack.cpp:53
void showModal(IView *modal)
uint8_t depth() const
Definition ViewStack.h:83
CDC Badge OS plugin host API - canonical C ABI contract.
#define HOST_ERR_INVALID_ARG
Definition host_api.h:39
void plg_ble_on_unload(void *plugin)
void plg_ble_pump(void)
void plg_ext_feature_pump(void)
void plg_ext_feature_on_unload(void *plugin)
void plg_gpio_on_unload(void *plugin)
void plg_http_on_unload(void *plugin)
void plg_msg_pump(void)
void plg_msg_on_unload(void *plugin)
void plg_msg_init(void)
void plg_net_pump(void)
void plg_net_on_unload(void *plugin)
void plg_socket_on_unload(void *plugin)
void plg_surface_on_unload(void *plugin)
FilePtr openFile(const char *path, const char *mode) noexcept
Open a FILE* and wrap it in a FilePtr.
Definition Raii.h:87
void unregister_host_imports()
Unregister the imports (called from PluginManager::deinit()).
uint8_t collectLockscreenItems(LockscreenRegistration *out, uint8_t max)
static const char * TAG
void clearLockscreenRegistrationFor(void *plugin)
bool register_host_imports()
Register the "cdc" import namespace with WAMR.
const char * tr(const char *key)
Look up a translation by string key.
Definition I18n.h:209
bool autoload
Start this plugin as a resident background instance at badge boot. Plugins without this flag stay unl...
std::vector< std::string > message_types
std::vector< std::string > provides
static bool parse(const char *json, size_t len, PluginManifest &out)
Parse meta.json content. Returns false on schema errors.
std::map< std::string, LocalizedString > i18n_strings