diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..8450693 --- /dev/null +++ b/.clang-format @@ -0,0 +1,11 @@ +BasedOnStyle: LLVM +ColumnLimit: 100 +BinPackArguments: false +BinPackParameters: false +AllowAllArgumentsOnNextLine: false +AlignAfterOpenBracket: BlockIndent +UseTab: ForIndentation +IndentWidth: 4 +TabWidth: 4 +ContinuationIndentWidth: 4 +AllowShortFunctionsOnASingleLine: None diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d89c76d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 + +[*.{c,cc,cpp,h,hpp,ino}] +indent_style = tab +indent_size = tab +tab_width = 4 diff --git a/.gitignore b/.gitignore index 78f49b6..6346d5c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ .venv build/ build_prev_runner/ -.vscode \ No newline at end of file diff --git a/.vscode/bin/clang-format b/.vscode/bin/clang-format new file mode 100755 index 0000000..0df371f --- /dev/null +++ b/.vscode/bin/clang-format @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if command -v clang-format >/dev/null 2>&1; then + exec clang-format "$@" +fi + +_home_dir="${HOME:-}" +if [ -n "$_home_dir" ]; then + _candidate="$(ls -1d "$_home_dir"/.vscode/extensions/ms-vscode.cpptools-*-linux-x64/LLVM/bin/clang-format 2>/dev/null | tail -n 1 || true)" + if [ -n "$_candidate" ] && [ -x "$_candidate" ]; then + exec "$_candidate" "$@" + fi +fi + +echo "clang-format executable not found." >&2 +echo "Install clang-format system-wide or install/update ms-vscode.cpptools." >&2 +exit 127 diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..f814711 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + "recommendations": [ + "pioarduino.pioarduino-ide", + "xaver.clang-format" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..24368c8 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,30 @@ +{ + "files.associations": { + "*.ino": "cpp" + }, + "editor.defaultFormatter": "xaver.clang-format", + "C_Cpp.formatting": "Disabled", + "clang-format.style": "file", + "clang-format.executable": "${workspaceRoot}/.vscode/bin/clang-format", + "[cpp]": { + "editor.defaultFormatter": "xaver.clang-format", + "editor.detectIndentation": false, + "editor.insertSpaces": false, + "editor.tabSize": 4, + "editor.formatOnSave": true + }, + "[c]": { + "editor.defaultFormatter": "xaver.clang-format", + "editor.detectIndentation": false, + "editor.insertSpaces": false, + "editor.tabSize": 4, + "editor.formatOnSave": true + }, + "[arduino]": { + "editor.defaultFormatter": "xaver.clang-format", + "editor.detectIndentation": false, + "editor.insertSpaces": false, + "editor.tabSize": 4, + "editor.formatOnSave": true + } +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..20e66d5 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,12 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Format Firmware Sources", + "type": "shell", + "command": "bash ${workspaceFolder}/scripts/format_cpp.sh", + "group": "build", + "problemMatcher": [] + } + ] +} diff --git a/README.md b/README.md index 8ce5bb7..2fe9ce2 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,13 @@ serializeJson(json, Serial); - `examples/failure-policy` - rollback and teardown failure policy behavior. - `examples/reload-burst` - listener burst coalescing and deduplicated node-name reloads. +## Formatting Baseline + +This repository follows the firmware formatting baseline from `esptoolkit-template`: +- `.clang-format` is the source of truth for C/C++/INO layout. +- `.editorconfig` enforces tabs (`tab_width = 4`), LF endings, and final newline. +- Format all tracked firmware sources with `bash scripts/format_cpp.sh`. + ## License MIT - see [LICENSE.md](LICENSE.md). diff --git a/examples/basic-startup/basic-startup.ino b/examples/basic-startup/basic-startup.ino index 3642e95..02271f2 100644 --- a/examples/basic-startup/basic-startup.ino +++ b/examples/basic-startup/basic-startup.ino @@ -6,48 +6,53 @@ ESPWorker worker; ESPLifecycle lifecycle; void setup() { - Serial.begin(115200); - - worker.init(ESPWorker::Config{}); - - LifecycleConfig config{}; - config.worker = &worker; - config.enableParallelInit = true; - config.enableParallelDeinit = true; - config.enableParallelReinit = true; - config.onInitStarted = []() { Serial.println("init started"); }; - config.onReady = []() { Serial.println("running"); }; - config.onInitFailed = []() { Serial.println("init failed"); }; - - lifecycle.configure(config); - lifecycle.init({"core", "network"}); - - lifecycle.addTo("core", "logger", []() { return true; }, []() { return true; }).parallelSafe(); - lifecycle.addTo("core", "storage", []() { return true; }, []() { return true; }); - lifecycle.addTo("network", "wifi", []() { return true; }, []() { return true; }).after("storage"); - - LifecycleResult buildResult = lifecycle.build(); - if( !buildResult.ok ){ - Serial.println("build failed"); - return; - } - - LifecycleResult initResult = lifecycle.initialize(); - if( !initResult.ok ){ - Serial.println("initialize failed"); - return; - } - - JsonDocument snapshot = lifecycle.snapshotJson(); - const bool phaseCompleted = snapshot["phaseCompleted"] | false; - Serial.printf("phase status: %s\n", phaseCompleted ? "completed" : "in progress"); - serializeJson(snapshot, Serial); - Serial.println(); - - delay(1000); - (void)lifecycle.deinitialize({"logger"}); + Serial.begin(115200); + + worker.init(ESPWorker::Config{}); + + LifecycleConfig config{}; + config.worker = &worker; + config.enableParallelInit = true; + config.enableParallelDeinit = true; + config.enableParallelReinit = true; + config.onInitStarted = []() { Serial.println("init started"); }; + config.onReady = []() { Serial.println("running"); }; + config.onInitFailed = []() { Serial.println("init failed"); }; + + lifecycle.configure(config); + lifecycle.init({"core", "network"}); + + lifecycle.addTo("core", "logger", []() { return true; }, []() { return true; }).parallelSafe(); + lifecycle.addTo("core", "storage", []() { return true; }, []() { return true; }); + lifecycle.addTo( + "network", + "wifi", + []() { return true; }, + []() { return true; } + ).after("storage"); + + LifecycleResult buildResult = lifecycle.build(); + if (!buildResult.ok) { + Serial.println("build failed"); + return; + } + + LifecycleResult initResult = lifecycle.initialize(); + if (!initResult.ok) { + Serial.println("initialize failed"); + return; + } + + JsonDocument snapshot = lifecycle.snapshotJson(); + const bool phaseCompleted = snapshot["phaseCompleted"] | false; + Serial.printf("phase status: %s\n", phaseCompleted ? "completed" : "in progress"); + serializeJson(snapshot, Serial); + Serial.println(); + + delay(1000); + (void)lifecycle.deinitialize({"logger"}); } void loop() { - delay(250); + delay(250); } diff --git a/examples/deferred-readiness/deferred-readiness.ino b/examples/deferred-readiness/deferred-readiness.ino index 37e8add..59f0cdc 100644 --- a/examples/deferred-readiness/deferred-readiness.ino +++ b/examples/deferred-readiness/deferred-readiness.ino @@ -6,57 +6,57 @@ ESPWorker worker; ESPLifecycle lifecycle; bool initCore() { - Serial.println("init core"); - return true; + Serial.println("init core"); + return true; } bool initCloud() { - Serial.println("init cloud"); - return true; + Serial.println("init cloud"); + return true; } bool deinitCore() { - Serial.println("deinit core"); - return true; + Serial.println("deinit core"); + return true; } bool deinitCloud() { - Serial.println("deinit cloud"); - return true; + Serial.println("deinit cloud"); + return true; } bool cloudReady() { - return millis() > 4000; + return millis() > 4000; } void waitForReady(TickType_t waitTicks) { - (void)waitTicks; - delay(250); + (void)waitTicks; + delay(250); } void setup() { - Serial.begin(115200); + Serial.begin(115200); - worker.init(ESPWorker::Config{}); + worker.init(ESPWorker::Config{}); - LifecycleConfig config{}; - config.worker = &worker; - config.onReady = []() { Serial.println("all sections ready"); }; + LifecycleConfig config{}; + config.worker = &worker; + config.onReady = []() { Serial.println("all sections ready"); }; - lifecycle.configure(config); - lifecycle.init({"core", "cloud"}); + lifecycle.configure(config); + lifecycle.init({"core", "cloud"}); - lifecycle.section("cloud") - .mode(LifecycleSectionMode::Deferred) - .readiness(cloudReady, waitForReady); + lifecycle.section("cloud") + .mode(LifecycleSectionMode::Deferred) + .readiness(cloudReady, waitForReady); - lifecycle.addTo("core", "core-init", initCore, deinitCore); - lifecycle.addTo("cloud", "cloud-sync", initCloud, deinitCloud).after("core-init"); + lifecycle.addTo("core", "core-init", initCore, deinitCore); + lifecycle.addTo("cloud", "cloud-sync", initCloud, deinitCloud).after("core-init"); - (void)lifecycle.build(); - (void)lifecycle.initialize(); + (void)lifecycle.build(); + (void)lifecycle.initialize(); } void loop() { - delay(500); + delay(500); } diff --git a/examples/dependency-closure/dependency-closure.ino b/examples/dependency-closure/dependency-closure.ino index ee1472e..8257a47 100644 --- a/examples/dependency-closure/dependency-closure.ino +++ b/examples/dependency-closure/dependency-closure.ino @@ -10,76 +10,76 @@ static bool wifiReady = false; static bool apiReady = false; bool initLogger() { - loggerReady = true; - Serial.println("init logger"); - return true; + loggerReady = true; + Serial.println("init logger"); + return true; } bool initWifi() { - if( !loggerReady ){ - return false; - } - wifiReady = true; - Serial.println("init wifi"); - return true; + if (!loggerReady) { + return false; + } + wifiReady = true; + Serial.println("init wifi"); + return true; } bool initApi() { - if( !wifiReady ){ - return false; - } - apiReady = true; - Serial.println("init api"); - return true; + if (!wifiReady) { + return false; + } + apiReady = true; + Serial.println("init api"); + return true; } bool deinitLogger() { - loggerReady = false; - Serial.println("deinit logger"); - return true; + loggerReady = false; + Serial.println("deinit logger"); + return true; } bool deinitWifi() { - wifiReady = false; - Serial.println("deinit wifi"); - return true; + wifiReady = false; + Serial.println("deinit wifi"); + return true; } bool deinitApi() { - apiReady = false; - Serial.println("deinit api"); - return true; + apiReady = false; + Serial.println("deinit api"); + return true; } void setup() { - Serial.begin(115200); + Serial.begin(115200); - worker.init(ESPWorker::Config{}); + worker.init(ESPWorker::Config{}); - LifecycleConfig config{}; - config.worker = &worker; - config.enableParallelInit = false; - config.enableParallelDeinit = false; - config.enableParallelReinit = false; - config.dependencyReinitialization = true; + LifecycleConfig config{}; + config.worker = &worker; + config.enableParallelInit = false; + config.enableParallelDeinit = false; + config.enableParallelReinit = false; + config.dependencyReinitialization = true; - lifecycle.configure(config); - lifecycle.init({"core", "network", "services"}); + lifecycle.configure(config); + lifecycle.init({"core", "network", "services"}); - lifecycle.addTo("core", "logger", initLogger, deinitLogger); - lifecycle.addTo("network", "wifi", initWifi, deinitWifi).after("logger"); - lifecycle.addTo("services", "api", initApi, deinitApi).after("wifi"); + lifecycle.addTo("core", "logger", initLogger, deinitLogger); + lifecycle.addTo("network", "wifi", initWifi, deinitWifi).after("logger"); + lifecycle.addTo("services", "api", initApi, deinitApi).after("wifi"); - (void)lifecycle.build(); - (void)lifecycle.initialize(); + (void)lifecycle.build(); + (void)lifecycle.initialize(); - Serial.println("Reinitialize logger -> expects wifi+api closure"); - (void)lifecycle.reinitialize({"logger"}); + Serial.println("Reinitialize logger -> expects wifi+api closure"); + (void)lifecycle.reinitialize({"logger"}); - Serial.println("Deinitialize logger -> expects wifi+api teardown"); - (void)lifecycle.deinitialize({"logger"}); + Serial.println("Deinitialize logger -> expects wifi+api teardown"); + (void)lifecycle.deinitialize({"logger"}); } void loop() { - delay(500); + delay(500); } diff --git a/examples/failure-policy/failure-policy.ino b/examples/failure-policy/failure-policy.ino index 2af5d05..afee555 100644 --- a/examples/failure-policy/failure-policy.ino +++ b/examples/failure-policy/failure-policy.ino @@ -8,55 +8,55 @@ ESPLifecycle lifecycle; static uint8_t flakyAttempts = 0; bool initStable() { - Serial.println("init stable"); - return true; + Serial.println("init stable"); + return true; } bool initFlaky() { - flakyAttempts++; - Serial.printf("init flaky attempt %u\n", flakyAttempts); - return flakyAttempts > 1; + flakyAttempts++; + Serial.printf("init flaky attempt %u\n", flakyAttempts); + return flakyAttempts > 1; } bool deinitStable() { - Serial.println("deinit stable"); - return true; + Serial.println("deinit stable"); + return true; } bool deinitFlaky() { - Serial.println("deinit flaky (forced fail)"); - return false; + Serial.println("deinit flaky (forced fail)"); + return false; } void setup() { - Serial.begin(115200); + Serial.begin(115200); - worker.init(ESPWorker::Config{}); + worker.init(ESPWorker::Config{}); - LifecycleConfig cfg{}; - cfg.worker = &worker; - cfg.rollbackOnInitFailure = true; - cfg.continueTeardownOnFailure = true; - cfg.onInitFailed = []() { Serial.println("init failed callback"); }; + LifecycleConfig cfg{}; + cfg.worker = &worker; + cfg.rollbackOnInitFailure = true; + cfg.continueTeardownOnFailure = true; + cfg.onInitFailed = []() { Serial.println("init failed callback"); }; - lifecycle.configure(cfg); - lifecycle.init({"core"}); + lifecycle.configure(cfg); + lifecycle.init({"core"}); - lifecycle.addTo("core", "stable", initStable, deinitStable); - lifecycle.addTo("core", "flaky", initFlaky, deinitFlaky).after("stable"); + lifecycle.addTo("core", "stable", initStable, deinitStable); + lifecycle.addTo("core", "flaky", initFlaky, deinitFlaky).after("stable"); - (void)lifecycle.build(); + (void)lifecycle.build(); - LifecycleResult first = lifecycle.initialize(); - Serial.printf("first initialize ok=%d\n", first.ok ? 1 : 0); + LifecycleResult first = lifecycle.initialize(); + Serial.printf("first initialize ok=%d\n", first.ok ? 1 : 0); - LifecycleResult second = lifecycle.initialize(); - Serial.printf("second initialize ok=%d\n", second.ok ? 1 : 0); + LifecycleResult second = lifecycle.initialize(); + Serial.printf("second initialize ok=%d\n", second.ok ? 1 : 0); - // Continue teardown despite flaky deinit failure. - (void)lifecycle.deinitialize(); + // Continue teardown despite flaky deinit failure. + (void)lifecycle.deinitialize(); } void loop() { - delay(500); + delay(500); } diff --git a/examples/parallel-waves/parallel-waves.ino b/examples/parallel-waves/parallel-waves.ino index 684767b..f3a6630 100644 --- a/examples/parallel-waves/parallel-waves.ino +++ b/examples/parallel-waves/parallel-waves.ino @@ -6,61 +6,61 @@ ESPWorker worker; ESPLifecycle lifecycle; bool initStorage() { - delay(50); - Serial.println("init storage"); - return true; + delay(50); + Serial.println("init storage"); + return true; } bool initLogger() { - delay(50); - Serial.println("init logger"); - return true; + delay(50); + Serial.println("init logger"); + return true; } bool initCache() { - delay(25); - Serial.println("init cache"); - return true; + delay(25); + Serial.println("init cache"); + return true; } bool initTelemetry() { - delay(25); - Serial.println("init telemetry"); - return true; + delay(25); + Serial.println("init telemetry"); + return true; } bool deinitSimple() { - Serial.println("deinit step"); - return true; + Serial.println("deinit step"); + return true; } void setup() { - Serial.begin(115200); + Serial.begin(115200); - worker.init(ESPWorker::Config{}); + worker.init(ESPWorker::Config{}); - LifecycleConfig cfg{}; - cfg.worker = &worker; - cfg.enableParallelInit = true; - cfg.enableParallelDeinit = true; - cfg.enableParallelReinit = true; - cfg.dependencyReinitialization = true; + LifecycleConfig cfg{}; + cfg.worker = &worker; + cfg.enableParallelInit = true; + cfg.enableParallelDeinit = true; + cfg.enableParallelReinit = true; + cfg.dependencyReinitialization = true; - lifecycle.configure(cfg); - lifecycle.init({"core"}); + lifecycle.configure(cfg); + lifecycle.init({"core"}); - lifecycle.addTo("core", "storage", initStorage, deinitSimple); - lifecycle.addTo("core", "logger", initLogger, deinitSimple); - lifecycle.addTo("core", "cache", initCache, deinitSimple).after("storage").parallelSafe(); - lifecycle.addTo("core", "telemetry", initTelemetry, deinitSimple).after("storage"); + lifecycle.addTo("core", "storage", initStorage, deinitSimple); + lifecycle.addTo("core", "logger", initLogger, deinitSimple); + lifecycle.addTo("core", "cache", initCache, deinitSimple).after("storage").parallelSafe(); + lifecycle.addTo("core", "telemetry", initTelemetry, deinitSimple).after("storage"); - (void)lifecycle.build(); - (void)lifecycle.initialize(); + (void)lifecycle.build(); + (void)lifecycle.initialize(); - // Reinitialize storage should include dependent closure. - (void)lifecycle.reinitialize({"storage"}); + // Reinitialize storage should include dependent closure. + (void)lifecycle.reinitialize({"storage"}); } void loop() { - delay(500); + delay(500); } diff --git a/examples/reload-burst/reload-burst.ino b/examples/reload-burst/reload-burst.ino index 7afd394..4547db0 100644 --- a/examples/reload-burst/reload-burst.ino +++ b/examples/reload-burst/reload-burst.ino @@ -4,77 +4,96 @@ #include struct ReloadPayload { - const char* names[4]; - uint8_t count; + const char *names[4]; + uint8_t count; }; enum : uint16_t { - EVENT_RELOAD = 200, + EVENT_RELOAD = 200, }; ESPWorker worker; ESPEventBus eventBus; ESPLifecycle lifecycle; -bool initStep(const char* name) { - Serial.printf("init %s\n", name); - return true; +bool initStep(const char *name) { + Serial.printf("init %s\n", name); + return true; } -bool deinitStep(const char* name) { - Serial.printf("deinit %s\n", name); - return true; +bool deinitStep(const char *name) { + Serial.printf("deinit %s\n", name); + return true; } void setup() { - Serial.begin(115200); - - worker.init(ESPWorker::Config{}); - eventBus.init(EventBusConfig{}); - - LifecycleConfig cfg{}; - cfg.worker = &worker; - cfg.enableParallelReinit = true; - - lifecycle.configure(cfg); - lifecycle.init({"core", "network", "services"}); - - lifecycle.addTo("core", "logger", []() { return initStep("logger"); }, []() { return deinitStep("logger"); }); - lifecycle.addTo("network", "wifi", []() { return initStep("wifi"); }, []() { return deinitStep("wifi"); }).after("logger"); - lifecycle.addTo("services", "api", []() { return initStep("api"); }, []() { return deinitStep("api"); }).after("wifi"); - - (void)lifecycle.build(); - (void)lifecycle.initialize(); - - (void)lifecycle.startReloadListener( - eventBus, - EVENT_RELOAD, - [](void* payload) -> std::vector { - std::vector names; - if( payload == nullptr ){ - return names; - } - - ReloadPayload* p = static_cast(payload); - for( uint8_t i = 0; i < p->count; i++ ){ - if( p->names[i] != nullptr ){ - names.push_back(p->names[i]); - } - } - return names; - } - ); - - // Burst: should coalesce and deduplicate names. - ReloadPayload p1{{"logger", "wifi", nullptr, nullptr}, 2}; - ReloadPayload p2{{"wifi", "api", nullptr, nullptr}, 2}; - ReloadPayload p3{{"logger", nullptr, nullptr, nullptr}, 1}; - - (void)eventBus.post(EVENT_RELOAD, &p1); - (void)eventBus.post(EVENT_RELOAD, &p2); - (void)eventBus.post(EVENT_RELOAD, &p3); + Serial.begin(115200); + + worker.init(ESPWorker::Config{}); + eventBus.init(EventBusConfig{}); + + LifecycleConfig cfg{}; + cfg.worker = &worker; + cfg.enableParallelReinit = true; + + lifecycle.configure(cfg); + lifecycle.init({"core", "network", "services"}); + + lifecycle.addTo( + "core", + "logger", + []() { return initStep("logger"); }, + []() { return deinitStep("logger"); } + ); + lifecycle + .addTo( + "network", + "wifi", + []() { return initStep("wifi"); }, + []() { return deinitStep("wifi"); } + ) + .after("logger"); + lifecycle + .addTo( + "services", + "api", + []() { return initStep("api"); }, + []() { return deinitStep("api"); } + ) + .after("wifi"); + + (void)lifecycle.build(); + (void)lifecycle.initialize(); + + (void)lifecycle.startReloadListener( + eventBus, + EVENT_RELOAD, + [](void *payload) -> std::vector { + std::vector names; + if (payload == nullptr) { + return names; + } + + ReloadPayload *p = static_cast(payload); + for (uint8_t i = 0; i < p->count; i++) { + if (p->names[i] != nullptr) { + names.push_back(p->names[i]); + } + } + return names; + } + ); + + // Burst: should coalesce and deduplicate names. + ReloadPayload p1{{"logger", "wifi", nullptr, nullptr}, 2}; + ReloadPayload p2{{"wifi", "api", nullptr, nullptr}, 2}; + ReloadPayload p3{{"logger", nullptr, nullptr, nullptr}, 1}; + + (void)eventBus.post(EVENT_RELOAD, &p1); + (void)eventBus.post(EVENT_RELOAD, &p2); + (void)eventBus.post(EVENT_RELOAD, &p3); } void loop() { - delay(500); + delay(500); } diff --git a/examples/scoped-reload/scoped-reload.ino b/examples/scoped-reload/scoped-reload.ino index 2ed6689..9424952 100644 --- a/examples/scoped-reload/scoped-reload.ino +++ b/examples/scoped-reload/scoped-reload.ino @@ -4,12 +4,12 @@ #include struct ReloadPayload { - const char* names[3]; - uint8_t count; + const char *names[3]; + uint8_t count; }; enum : uint16_t { - EVENT_RELOAD_NODES = 100, + EVENT_RELOAD_NODES = 100, }; ESPWorker worker; @@ -17,62 +17,69 @@ ESPEventBus eventBus; ESPLifecycle lifecycle; void setup() { - Serial.begin(115200); + Serial.begin(115200); - worker.init(ESPWorker::Config{}); - eventBus.init(EventBusConfig{}); + worker.init(ESPWorker::Config{}); + eventBus.init(EventBusConfig{}); - LifecycleConfig config{}; - config.worker = &worker; - config.enableParallelInit = true; - config.enableParallelDeinit = true; - config.enableParallelReinit = true; - config.onSnapshot = [](const LifecycleSnapshot& snapshot) { - Serial.printf("state=%d active=%s completed=%u/%u\n", - static_cast(snapshot.state), - snapshot.activeNode == nullptr ? "-" : snapshot.activeNode, - snapshot.completed, - snapshot.total); - }; + LifecycleConfig config{}; + config.worker = &worker; + config.enableParallelInit = true; + config.enableParallelDeinit = true; + config.enableParallelReinit = true; + config.onSnapshot = [](const LifecycleSnapshot &snapshot) { + Serial.printf( + "state=%d active=%s completed=%u/%u\n", + static_cast(snapshot.state), + snapshot.activeNode == nullptr ? "-" : snapshot.activeNode, + snapshot.completed, + snapshot.total + ); + }; - lifecycle.configure(config); - lifecycle.init({"core", "network", "services"}); + lifecycle.configure(config); + lifecycle.init({"core", "network", "services"}); - lifecycle.addTo("core", "logger", []() { return true; }, []() { return true; }); - lifecycle.addTo("network", "wifi", []() { return true; }, []() { return true; }).after("logger"); - lifecycle.addTo("services", "api", []() { return true; }, []() { return true; }).after("wifi"); + lifecycle.addTo("core", "logger", []() { return true; }, []() { return true; }); + lifecycle.addTo( + "network", + "wifi", + []() { return true; }, + []() { return true; } + ).after("logger"); + lifecycle.addTo("services", "api", []() { return true; }, []() { return true; }).after("wifi"); - if( !lifecycle.startReloadListener( - eventBus, - EVENT_RELOAD_NODES, - [](void* payload) -> std::vector { - std::vector names; - if( payload == nullptr ){ - return names; - } + if (!lifecycle.startReloadListener( + eventBus, + EVENT_RELOAD_NODES, + [](void *payload) -> std::vector { + std::vector names; + if (payload == nullptr) { + return names; + } - ReloadPayload* data = static_cast(payload); - for( uint8_t i = 0; i < data->count; i++ ){ - if( data->names[i] != nullptr ){ - names.push_back(data->names[i]); - } - } - return names; - } - ) ){ - Serial.println("reload listener failed"); - } + ReloadPayload *data = static_cast(payload); + for (uint8_t i = 0; i < data->count; i++) { + if (data->names[i] != nullptr) { + names.push_back(data->names[i]); + } + } + return names; + } + )) { + Serial.println("reload listener failed"); + } - (void)lifecycle.build(); - (void)lifecycle.initialize(); + (void)lifecycle.build(); + (void)lifecycle.initialize(); - ReloadPayload payload{{"logger", nullptr, nullptr}, 1}; - (void)eventBus.post(EVENT_RELOAD_NODES, &payload); + ReloadPayload payload{{"logger", nullptr, nullptr}, 1}; + (void)eventBus.post(EVENT_RELOAD_NODES, &payload); - delay(1000); - (void)lifecycle.deinitialize({"logger"}); + delay(1000); + (void)lifecycle.deinitialize({"logger"}); } void loop() { - delay(500); + delay(500); } diff --git a/scripts/format_cpp.sh b/scripts/format_cpp.sh new file mode 100755 index 0000000..7d17b04 --- /dev/null +++ b/scripts/format_cpp.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +set -euo pipefail + +_repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" +_clang_format="${_repo_root}/.vscode/bin/clang-format" + +if [ ! -x "${_clang_format}" ]; then + echo "clang-format wrapper not found: ${_clang_format}" >&2 + exit 1 +fi + +mapfile -d '' _format_files < <( + git -C "${_repo_root}" ls-files -z -- '*.c' '*.cc' '*.cpp' '*.h' '*.hpp' '*.ino' +) + +if [ "${#_format_files[@]}" -eq 0 ]; then + echo "No tracked C/C++/INO files found to format." + exit 0 +fi + +"${_clang_format}" -i --style=file "${_format_files[@]}" + +echo "Formatted ${#_format_files[@]} files." diff --git a/src/ESPLifecycle.cpp b/src/ESPLifecycle.cpp index 11b6954..df435de 100644 --- a/src/ESPLifecycle.cpp +++ b/src/ESPLifecycle.cpp @@ -12,305 +12,298 @@ #include #endif -ESPLifecycle::NodeBuilder::NodeBuilder(ESPLifecycle* lifecycleRef, size_t nodeIndexRef) - : lifecycle(lifecycleRef), nodeIndex(nodeIndexRef) {} +ESPLifecycle::NodeBuilder::NodeBuilder(ESPLifecycle *lifecycleRef, size_t nodeIndexRef) + : lifecycle(lifecycleRef), nodeIndex(nodeIndexRef) { +} -ESPLifecycle::NodeBuilder& ESPLifecycle::NodeBuilder::after(const char* dependencyNodeName) { - if( lifecycle != nullptr ){ - lifecycle->addDependencyName(nodeIndex, dependencyNodeName); - } - return *this; +ESPLifecycle::NodeBuilder &ESPLifecycle::NodeBuilder::after(const char *dependencyNodeName) { + if (lifecycle != nullptr) { + lifecycle->addDependencyName(nodeIndex, dependencyNodeName); + } + return *this; } -ESPLifecycle::NodeBuilder& -ESPLifecycle::NodeBuilder::after(std::initializer_list dependencyNodeNames) { - if( lifecycle != nullptr ){ - for( const char* dependencyNodeName : dependencyNodeNames ){ - lifecycle->addDependencyName(nodeIndex, dependencyNodeName); - } - } - return *this; +ESPLifecycle::NodeBuilder & +ESPLifecycle::NodeBuilder::after(std::initializer_list dependencyNodeNames) { + if (lifecycle != nullptr) { + for (const char *dependencyNodeName : dependencyNodeNames) { + lifecycle->addDependencyName(nodeIndex, dependencyNodeName); + } + } + return *this; } -ESPLifecycle::NodeBuilder& ESPLifecycle::NodeBuilder::before(const char* dependentNodeName) { - if( lifecycle != nullptr ){ - lifecycle->addDependentName(nodeIndex, dependentNodeName); - } - return *this; +ESPLifecycle::NodeBuilder &ESPLifecycle::NodeBuilder::before(const char *dependentNodeName) { + if (lifecycle != nullptr) { + lifecycle->addDependentName(nodeIndex, dependentNodeName); + } + return *this; } -ESPLifecycle::NodeBuilder& ESPLifecycle::NodeBuilder::timeoutMs(uint32_t value) { - if( lifecycle != nullptr ){ - lifecycle->setNodeTimeout(nodeIndex, value); - } - return *this; +ESPLifecycle::NodeBuilder &ESPLifecycle::NodeBuilder::timeoutMs(uint32_t value) { + if (lifecycle != nullptr) { + lifecycle->setNodeTimeout(nodeIndex, value); + } + return *this; } -ESPLifecycle::NodeBuilder& ESPLifecycle::NodeBuilder::optional(bool isOptional) { - if( lifecycle != nullptr ){ - lifecycle->setNodeOptional(nodeIndex, isOptional); - } - return *this; +ESPLifecycle::NodeBuilder &ESPLifecycle::NodeBuilder::optional(bool isOptional) { + if (lifecycle != nullptr) { + lifecycle->setNodeOptional(nodeIndex, isOptional); + } + return *this; } -ESPLifecycle::NodeBuilder& ESPLifecycle::NodeBuilder::parallelSafe(bool enabled) { - if( lifecycle != nullptr ){ - lifecycle->setNodeParallelSafe(nodeIndex, enabled); - } - return *this; +ESPLifecycle::NodeBuilder &ESPLifecycle::NodeBuilder::parallelSafe(bool enabled) { + if (lifecycle != nullptr) { + lifecycle->setNodeParallelSafe(nodeIndex, enabled); + } + return *this; } -ESPLifecycle::SectionBuilder::SectionBuilder(ESPLifecycle* lifecycleRef, size_t sectionIndexRef) - : lifecycle(lifecycleRef), sectionIndex(sectionIndexRef) {} +ESPLifecycle::SectionBuilder::SectionBuilder(ESPLifecycle *lifecycleRef, size_t sectionIndexRef) + : lifecycle(lifecycleRef), sectionIndex(sectionIndexRef) { +} -ESPLifecycle::SectionBuilder& ESPLifecycle::SectionBuilder::mode(LifecycleSectionMode sectionMode) { - if( lifecycle != nullptr ){ - lifecycle->setSectionMode(sectionIndex, sectionMode); - } - return *this; +ESPLifecycle::SectionBuilder &ESPLifecycle::SectionBuilder::mode(LifecycleSectionMode sectionMode) { + if (lifecycle != nullptr) { + lifecycle->setSectionMode(sectionIndex, sectionMode); + } + return *this; } -ESPLifecycle::SectionBuilder& ESPLifecycle::SectionBuilder::readiness( - std::function isReady, - std::function waitFn +ESPLifecycle::SectionBuilder &ESPLifecycle::SectionBuilder::readiness( + std::function isReady, std::function waitFn ) { - if( lifecycle != nullptr ){ - lifecycle->setSectionReadiness(sectionIndex, isReady, waitFn); - } - return *this; + if (lifecycle != nullptr) { + lifecycle->setSectionReadiness(sectionIndex, isReady, waitFn); + } + return *this; } -ESPLifecycle& ESPLifecycle::init(std::initializer_list sectionNames) { - if( sections.empty() ){ - for( const char* sectionName : sectionNames ){ - ensureSection(sectionName); - } - } +ESPLifecycle &ESPLifecycle::init(std::initializer_list sectionNames) { + if (sections.empty()) { + for (const char *sectionName : sectionNames) { + ensureSection(sectionName); + } + } - if( sections.empty() ){ - ensureSection("default"); - } + if (sections.empty()) { + ensureSection("default"); + } - return *this; + return *this; } -ESPLifecycle::SectionBuilder& ESPLifecycle::section(const char* sectionName) { - const size_t index = ensureSection(sectionName); - sectionBuilder = SectionBuilder(this, index); - return sectionBuilder; +ESPLifecycle::SectionBuilder &ESPLifecycle::section(const char *sectionName) { + const size_t index = ensureSection(sectionName); + sectionBuilder = SectionBuilder(this, index); + return sectionBuilder; } -ESPLifecycle::NodeBuilder& ESPLifecycle::addTo( - const char* section, - const char* nodeName, - std::function initFn -) { - return addTo(section, nodeName, std::move(initFn), []() { return true; }); +ESPLifecycle::NodeBuilder & +ESPLifecycle::addTo(const char *section, const char *nodeName, std::function initFn) { + return addTo(section, nodeName, std::move(initFn), []() { return true; }); } -ESPLifecycle::NodeBuilder& ESPLifecycle::addTo( - const char* section, - const char* nodeName, +ESPLifecycle::NodeBuilder &ESPLifecycle::addTo( + const char *section, + const char *nodeName, std::function initFn, std::function teardownFn ) { - const size_t sectionIndex = ensureSection(section); - - LifecycleNodeDefinition node{}; - if( nodeName != nullptr ){ - node.name = nodeName; - } - node.sectionIndex = sectionIndex; - node.initFn = std::move(initFn); - node.teardownFn = std::move(teardownFn); - node.timeoutMs = config.defaultStepTimeoutMs; - nodes.push_back(std::move(node)); - - nodeBuilder = NodeBuilder(this, nodes.size() - 1); - graphBuilt = false; - return nodeBuilder; + const size_t sectionIndex = ensureSection(section); + + LifecycleNodeDefinition node{}; + if (nodeName != nullptr) { + node.name = nodeName; + } + node.sectionIndex = sectionIndex; + node.initFn = std::move(initFn); + node.teardownFn = std::move(teardownFn); + node.timeoutMs = config.defaultStepTimeoutMs; + nodes.push_back(std::move(node)); + + nodeBuilder = NodeBuilder(this, nodes.size() - 1); + graphBuilt = false; + return nodeBuilder; } -bool ESPLifecycle::configure(const LifecycleConfig& configValue) { - config = configValue; - return true; +bool ESPLifecycle::configure(const LifecycleConfig &configValue) { + config = configValue; + return true; } -LifecycleResult ESPLifecycle::deinitialize(std::initializer_list nodeNames) { - std::vector names(nodeNames.begin(), nodeNames.end()); - return deinitialize(names); +LifecycleResult ESPLifecycle::deinitialize(std::initializer_list nodeNames) { + std::vector names(nodeNames.begin(), nodeNames.end()); + return deinitialize(names); } -LifecycleResult ESPLifecycle::reinitialize(std::initializer_list nodeNames) { - std::vector names(nodeNames.begin(), nodeNames.end()); - return reinitialize(names); +LifecycleResult ESPLifecycle::reinitialize(std::initializer_list nodeNames) { + std::vector names(nodeNames.begin(), nodeNames.end()); + return reinitialize(names); } void ESPLifecycle::clear() { - stopReloadListener(); - - { - std::lock_guard lock(transitionMutex); - nodes.clear(); - sections.clear(); - topologicalOrder.clear(); - initialized.clear(); - graphBuilt = false; - } - - setState(LifecycleState::Idle, nullptr); - { - std::lock_guard lock(snapshotMutex); - snapshotValue.completed = 0; - snapshotValue.total = 0; - snapshotValue.failed = false; - snapshotValue.errorCode = LifecycleErrorCode::None; - detailText.clear(); - nodeNameText.clear(); - lastOperationOk = true; - phaseText = "idle"; - } - publishSnapshot(); + stopReloadListener(); + + { + std::lock_guard lock(transitionMutex); + nodes.clear(); + sections.clear(); + topologicalOrder.clear(); + initialized.clear(); + graphBuilt = false; + } + + setState(LifecycleState::Idle, nullptr); + { + std::lock_guard lock(snapshotMutex); + snapshotValue.completed = 0; + snapshotValue.total = 0; + snapshotValue.failed = false; + snapshotValue.errorCode = LifecycleErrorCode::None; + detailText.clear(); + nodeNameText.clear(); + lastOperationOk = true; + phaseText = "idle"; + } + publishSnapshot(); } LifecycleState ESPLifecycle::state() const { - std::lock_guard lock(stateMutex); - return currentState; + std::lock_guard lock(stateMutex); + return currentState; } -size_t ESPLifecycle::ensureSection(const char* sectionName) { - const std::string name = sectionName == nullptr ? "" : sectionName; - for( size_t index = 0; index < sections.size(); index++ ){ - if( sections[index].name == name ){ - return index; - } - } - - LifecycleSectionDefinition sectionDef{}; - sectionDef.name = name; - sections.push_back(std::move(sectionDef)); - graphBuilt = false; - return sections.size() - 1; +size_t ESPLifecycle::ensureSection(const char *sectionName) { + const std::string name = sectionName == nullptr ? "" : sectionName; + for (size_t index = 0; index < sections.size(); index++) { + if (sections[index].name == name) { + return index; + } + } + + LifecycleSectionDefinition sectionDef{}; + sectionDef.name = name; + sections.push_back(std::move(sectionDef)); + graphBuilt = false; + return sections.size() - 1; } -void ESPLifecycle::addDependencyName(size_t nodeIndex, const char* dependencyNodeName) { - if( dependencyNodeName == nullptr || nodeIndex >= nodes.size() ){ - return; - } - nodes[nodeIndex].dependenciesByName.emplace_back(dependencyNodeName); - graphBuilt = false; +void ESPLifecycle::addDependencyName(size_t nodeIndex, const char *dependencyNodeName) { + if (dependencyNodeName == nullptr || nodeIndex >= nodes.size()) { + return; + } + nodes[nodeIndex].dependenciesByName.emplace_back(dependencyNodeName); + graphBuilt = false; } -void ESPLifecycle::addDependentName(size_t nodeIndex, const char* dependentNodeName) { - if( dependentNodeName == nullptr || nodeIndex >= nodes.size() ){ - return; - } - nodes[nodeIndex].dependentsByName.emplace_back(dependentNodeName); - graphBuilt = false; +void ESPLifecycle::addDependentName(size_t nodeIndex, const char *dependentNodeName) { + if (dependentNodeName == nullptr || nodeIndex >= nodes.size()) { + return; + } + nodes[nodeIndex].dependentsByName.emplace_back(dependentNodeName); + graphBuilt = false; } void ESPLifecycle::setNodeTimeout(size_t nodeIndex, uint32_t value) { - if( nodeIndex >= nodes.size() ){ - return; - } - nodes[nodeIndex].timeoutMs = value; + if (nodeIndex >= nodes.size()) { + return; + } + nodes[nodeIndex].timeoutMs = value; } void ESPLifecycle::setNodeOptional(size_t nodeIndex, bool isOptional) { - if( nodeIndex >= nodes.size() ){ - return; - } - nodes[nodeIndex].optional = isOptional; + if (nodeIndex >= nodes.size()) { + return; + } + nodes[nodeIndex].optional = isOptional; } void ESPLifecycle::setNodeParallelSafe(size_t nodeIndex, bool enabled) { - if( nodeIndex >= nodes.size() ){ - return; - } - nodes[nodeIndex].parallelSafe = enabled; + if (nodeIndex >= nodes.size()) { + return; + } + nodes[nodeIndex].parallelSafe = enabled; } void ESPLifecycle::setSectionMode(size_t sectionIndex, LifecycleSectionMode mode) { - if( sectionIndex >= sections.size() ){ - return; - } - sections[sectionIndex].mode = mode; - graphBuilt = false; + if (sectionIndex >= sections.size()) { + return; + } + sections[sectionIndex].mode = mode; + graphBuilt = false; } void ESPLifecycle::setSectionReadiness( - size_t sectionIndex, - std::function isReady, - std::function waitFn + size_t sectionIndex, std::function isReady, std::function waitFn ) { - if( sectionIndex >= sections.size() ){ - return; - } - sections[sectionIndex].readinessCheck = std::move(isReady); - sections[sectionIndex].waitFn = std::move(waitFn); - graphBuilt = false; + if (sectionIndex >= sections.size()) { + return; + } + sections[sectionIndex].readinessCheck = std::move(isReady); + sections[sectionIndex].waitFn = std::move(waitFn); + graphBuilt = false; } -LifecycleResult ESPLifecycle::okResult(const char* detail) { - detailText = detail == nullptr ? "" : detail; - lastOperationOk = true; - LifecycleResult result{}; - result.ok = true; - result.code = LifecycleErrorCode::None; - result.nodeName = nullptr; - result.detail = detailText.empty() ? nullptr : detailText.c_str(); - return result; +LifecycleResult ESPLifecycle::okResult(const char *detail) { + detailText = detail == nullptr ? "" : detail; + lastOperationOk = true; + LifecycleResult result{}; + result.ok = true; + result.code = LifecycleErrorCode::None; + result.nodeName = nullptr; + result.detail = detailText.empty() ? nullptr : detailText.c_str(); + return result; } LifecycleResult ESPLifecycle::failResult( - LifecycleErrorCode code, - const char* nodeName, - const char* detail, - bool updateFailedState + LifecycleErrorCode code, const char *nodeName, const char *detail, bool updateFailedState ) { - nodeNameText = nodeName == nullptr ? "" : nodeName; - detailText = detail == nullptr ? "" : detail; - lastOperationOk = false; - - if( updateFailedState ){ - setState(LifecycleState::Failed, nodeNameText.empty() ? nullptr : nodeNameText.c_str()); - } - - { - std::lock_guard lock(snapshotMutex); - snapshotValue.failed = true; - snapshotValue.errorCode = code; - snapshotValue.updatedAtMs = nowMs(); - } - publishSnapshot(); - - LifecycleResult result{}; - result.ok = false; - result.code = code; - result.nodeName = nodeNameText.empty() ? nullptr : nodeNameText.c_str(); - result.detail = detailText.empty() ? nullptr : detailText.c_str(); - return result; + nodeNameText = nodeName == nullptr ? "" : nodeName; + detailText = detail == nullptr ? "" : detail; + lastOperationOk = false; + + if (updateFailedState) { + setState(LifecycleState::Failed, nodeNameText.empty() ? nullptr : nodeNameText.c_str()); + } + + { + std::lock_guard lock(snapshotMutex); + snapshotValue.failed = true; + snapshotValue.errorCode = code; + snapshotValue.updatedAtMs = nowMs(); + } + publishSnapshot(); + + LifecycleResult result{}; + result.ok = false; + result.code = code; + result.nodeName = nodeNameText.empty() ? nullptr : nodeNameText.c_str(); + result.detail = detailText.empty() ? nullptr : detailText.c_str(); + return result; } std::vector ESPLifecycle::allNodeIndexes() const { - std::vector indexes; - indexes.reserve(nodes.size()); - for( size_t i = 0; i < nodes.size(); i++ ){ - indexes.push_back(i); - } - return indexes; + std::vector indexes; + indexes.reserve(nodes.size()); + for (size_t i = 0; i < nodes.size(); i++) { + indexes.push_back(i); + } + return indexes; } -void ESPLifecycle::log(LifecycleLogLevel level, const char* message) const { - if( config.logger ){ - config.logger(level, message == nullptr ? "" : message); - } +void ESPLifecycle::log(LifecycleLogLevel level, const char *message) const { + if (config.logger) { + config.logger(level, message == nullptr ? "" : message); + } } uint32_t ESPLifecycle::nowMs() const { #if __has_include() - return static_cast(millis()); + return static_cast(millis()); #else - return 0; + return 0; #endif } diff --git a/src/esp_lifecycle/lifecycle.h b/src/esp_lifecycle/lifecycle.h index 8289bb5..50b563e 100644 --- a/src/esp_lifecycle/lifecycle.h +++ b/src/esp_lifecycle/lifecycle.h @@ -13,182 +13,162 @@ class ESPEventBus; class ESPLifecycle { public: - class NodeBuilder { - public: - NodeBuilder() = default; - NodeBuilder(ESPLifecycle* lifecycleRef, size_t nodeIndexRef); - - NodeBuilder& after(const char* dependencyNodeName); - NodeBuilder& after(std::initializer_list dependencyNodeNames); - NodeBuilder& before(const char* dependentNodeName); - NodeBuilder& timeoutMs(uint32_t value); - NodeBuilder& optional(bool isOptional); - NodeBuilder& parallelSafe(bool enabled = true); - - private: - ESPLifecycle* lifecycle = nullptr; - size_t nodeIndex = 0; - }; - - class SectionBuilder { - public: - SectionBuilder() = default; - SectionBuilder(ESPLifecycle* lifecycleRef, size_t sectionIndexRef); - - SectionBuilder& mode(LifecycleSectionMode sectionMode); - SectionBuilder& readiness(std::function isReady, std::function waitFn); - - private: - ESPLifecycle* lifecycle = nullptr; - size_t sectionIndex = 0; - }; - - ESPLifecycle() = default; - - ESPLifecycle& init(std::initializer_list sectionNames); - SectionBuilder& section(const char* sectionName); - NodeBuilder& addTo( - const char* section, - const char* nodeName, - std::function initFn - ); - NodeBuilder& addTo( - const char* section, - const char* nodeName, - std::function initFn, - std::function teardownFn - ); - - bool configure(const LifecycleConfig& config); - LifecycleResult build(); - - LifecycleResult initialize(); - LifecycleResult deinitialize(); - LifecycleResult deinitialize(std::initializer_list nodeNames); - LifecycleResult deinitialize(const std::vector& nodeNames); - - LifecycleResult reinitializeAll(); - LifecycleResult reinitialize(std::initializer_list nodeNames); - LifecycleResult reinitialize(const std::vector& nodeNames); - - bool startReloadListener( - ESPEventBus& eventBus, - uint16_t eventId, - std::function(void*)> payloadToNodeNames - ); - void stopReloadListener(); - - LifecycleState state() const; - LifecycleSnapshot snapshot() const; - JsonDocument snapshotJson() const; - void clear(); + class NodeBuilder { + public: + NodeBuilder() = default; + NodeBuilder(ESPLifecycle *lifecycleRef, size_t nodeIndexRef); + + NodeBuilder &after(const char *dependencyNodeName); + NodeBuilder &after(std::initializer_list dependencyNodeNames); + NodeBuilder &before(const char *dependentNodeName); + NodeBuilder &timeoutMs(uint32_t value); + NodeBuilder &optional(bool isOptional); + NodeBuilder ¶llelSafe(bool enabled = true); + + private: + ESPLifecycle *lifecycle = nullptr; + size_t nodeIndex = 0; + }; + + class SectionBuilder { + public: + SectionBuilder() = default; + SectionBuilder(ESPLifecycle *lifecycleRef, size_t sectionIndexRef); + + SectionBuilder &mode(LifecycleSectionMode sectionMode); + SectionBuilder & + readiness(std::function isReady, std::function waitFn); + + private: + ESPLifecycle *lifecycle = nullptr; + size_t sectionIndex = 0; + }; + + ESPLifecycle() = default; + + ESPLifecycle &init(std::initializer_list sectionNames); + SectionBuilder §ion(const char *sectionName); + NodeBuilder &addTo(const char *section, const char *nodeName, std::function initFn); + NodeBuilder &addTo( + const char *section, + const char *nodeName, + std::function initFn, + std::function teardownFn + ); + + bool configure(const LifecycleConfig &config); + LifecycleResult build(); + + LifecycleResult initialize(); + LifecycleResult deinitialize(); + LifecycleResult deinitialize(std::initializer_list nodeNames); + LifecycleResult deinitialize(const std::vector &nodeNames); + + LifecycleResult reinitializeAll(); + LifecycleResult reinitialize(std::initializer_list nodeNames); + LifecycleResult reinitialize(const std::vector &nodeNames); + + bool startReloadListener( + ESPEventBus &eventBus, + uint16_t eventId, + std::function(void *)> payloadToNodeNames + ); + void stopReloadListener(); + + LifecycleState state() const; + LifecycleSnapshot snapshot() const; + JsonDocument snapshotJson() const; + void clear(); private: - LifecycleConfig config = {}; - - std::vector sections = {}; - std::vector nodes = {}; - std::vector topologicalOrder = {}; - std::vector initialized = {}; - bool graphBuilt = false; - - mutable std::mutex stateMutex; - LifecycleState currentState = LifecycleState::Idle; - - mutable std::mutex snapshotMutex; - LifecycleSnapshot snapshotValue = {}; - std::string activeNodeText = {}; - std::string detailText = {}; - std::string nodeNameText = {}; - bool lastOperationOk = true; - std::string phaseText = "idle"; - - NodeBuilder nodeBuilder = {}; - SectionBuilder sectionBuilder = {}; - - mutable std::mutex transitionMutex; - - mutable std::mutex listenerMutex; - ESPEventBus* listenerBus = nullptr; - uint32_t listenerSubId = 0; - uint16_t listenerEventId = 0; - std::function(void*)> listenerPayloadNamesFn; - std::vector pendingNodeNames = {}; - bool listenerWorkerRunning = false; - uint32_t listenerCoalesceMs = 25; - - size_t ensureSection(const char* sectionName); - void addDependencyName(size_t nodeIndex, const char* dependencyNodeName); - void addDependentName(size_t nodeIndex, const char* dependentNodeName); - void setNodeTimeout(size_t nodeIndex, uint32_t value); - void setNodeOptional(size_t nodeIndex, bool isOptional); - void setNodeParallelSafe(size_t nodeIndex, bool enabled); - void setSectionMode(size_t sectionIndex, LifecycleSectionMode mode); - void setSectionReadiness( - size_t sectionIndex, - std::function isReady, - std::function waitFn - ); - - LifecycleResult failResult( - LifecycleErrorCode code, - const char* nodeName, - const char* detail, - bool updateFailedState - ); - LifecycleResult okResult(const char* detail = nullptr); - - LifecycleResult validateAndBuildGraph(); - LifecycleResult resolveIndexes(); - LifecycleResult ensureNoCycles(); - - LifecycleResult initializeInternal( - const std::vector& subset, - LifecycleState transitionState, - bool enableParallel - ); - LifecycleResult deinitializeInternal( - const std::vector& subset, - bool updateState, - bool enableParallel - ); - LifecycleResult runSectionInitialize( - size_t sectionIndex, - const std::vector& subset, - bool enableParallel - ); - LifecycleResult runPhaseBatches( - const std::vector>& batches, - bool initializePhase, - bool enableParallel - ); - LifecycleResult runParallelBatch(const std::vector& batch, bool initializePhase); - LifecycleResult runNodeInit(size_t nodeIndex, bool countProgress = true); - LifecycleResult runNodeTeardown(size_t nodeIndex, bool countProgress = true); - std::vector> buildWavesForSubset( - const std::vector& subset, - bool initializePhase - ) const; - bool requiresParallelWorkerForBatch(const std::vector& batch) const; - bool isParallelEligible(size_t nodeIndex) const; - - std::vector allNodeIndexes() const; - LifecycleResult resolveNodeNamesToSubset( - const std::vector& nodeNames, - std::vector& outSubset - ); - LifecycleResult expandSubsetWithDependents(std::vector& subset); - LifecycleResult expandSubsetWithDependencies(std::vector& subset); - LifecycleResult expandSubsetForReinitialize(std::vector& subset); - - void publishSnapshot(); - void setState(LifecycleState stateValue, const char* activeNode = nullptr); - void setPhase(const char* phaseName); - void markProgress(const char* activeNode, bool completedStep); - uint32_t nowMs() const; - - void log(LifecycleLogLevel level, const char* message) const; - - void scheduleNodeReinitialize(const std::vector& nodeNames); - void listenerWorkerLoop(); + LifecycleConfig config = {}; + + std::vector sections = {}; + std::vector nodes = {}; + std::vector topologicalOrder = {}; + std::vector initialized = {}; + bool graphBuilt = false; + + mutable std::mutex stateMutex; + LifecycleState currentState = LifecycleState::Idle; + + mutable std::mutex snapshotMutex; + LifecycleSnapshot snapshotValue = {}; + std::string activeNodeText = {}; + std::string detailText = {}; + std::string nodeNameText = {}; + bool lastOperationOk = true; + std::string phaseText = "idle"; + + NodeBuilder nodeBuilder = {}; + SectionBuilder sectionBuilder = {}; + + mutable std::mutex transitionMutex; + + mutable std::mutex listenerMutex; + ESPEventBus *listenerBus = nullptr; + uint32_t listenerSubId = 0; + uint16_t listenerEventId = 0; + std::function(void *)> listenerPayloadNamesFn; + std::vector pendingNodeNames = {}; + bool listenerWorkerRunning = false; + uint32_t listenerCoalesceMs = 25; + + size_t ensureSection(const char *sectionName); + void addDependencyName(size_t nodeIndex, const char *dependencyNodeName); + void addDependentName(size_t nodeIndex, const char *dependentNodeName); + void setNodeTimeout(size_t nodeIndex, uint32_t value); + void setNodeOptional(size_t nodeIndex, bool isOptional); + void setNodeParallelSafe(size_t nodeIndex, bool enabled); + void setSectionMode(size_t sectionIndex, LifecycleSectionMode mode); + void setSectionReadiness( + size_t sectionIndex, std::function isReady, std::function waitFn + ); + + LifecycleResult failResult( + LifecycleErrorCode code, const char *nodeName, const char *detail, bool updateFailedState + ); + LifecycleResult okResult(const char *detail = nullptr); + + LifecycleResult validateAndBuildGraph(); + LifecycleResult resolveIndexes(); + LifecycleResult ensureNoCycles(); + + LifecycleResult initializeInternal( + const std::vector &subset, LifecycleState transitionState, bool enableParallel + ); + LifecycleResult + deinitializeInternal(const std::vector &subset, bool updateState, bool enableParallel); + LifecycleResult runSectionInitialize( + size_t sectionIndex, const std::vector &subset, bool enableParallel + ); + LifecycleResult runPhaseBatches( + const std::vector> &batches, bool initializePhase, bool enableParallel + ); + LifecycleResult runParallelBatch(const std::vector &batch, bool initializePhase); + LifecycleResult runNodeInit(size_t nodeIndex, bool countProgress = true); + LifecycleResult runNodeTeardown(size_t nodeIndex, bool countProgress = true); + std::vector> + buildWavesForSubset(const std::vector &subset, bool initializePhase) const; + bool requiresParallelWorkerForBatch(const std::vector &batch) const; + bool isParallelEligible(size_t nodeIndex) const; + + std::vector allNodeIndexes() const; + LifecycleResult resolveNodeNamesToSubset( + const std::vector &nodeNames, std::vector &outSubset + ); + LifecycleResult expandSubsetWithDependents(std::vector &subset); + LifecycleResult expandSubsetWithDependencies(std::vector &subset); + LifecycleResult expandSubsetForReinitialize(std::vector &subset); + + void publishSnapshot(); + void setState(LifecycleState stateValue, const char *activeNode = nullptr); + void setPhase(const char *phaseName); + void markProgress(const char *activeNode, bool completedStep); + uint32_t nowMs() const; + + void log(LifecycleLogLevel level, const char *message) const; + + void scheduleNodeReinitialize(const std::vector &nodeNames); + void listenerWorkerLoop(); }; diff --git a/src/internal/GraphTypes.h b/src/internal/GraphTypes.h index bbe8e44..399abef 100644 --- a/src/internal/GraphTypes.h +++ b/src/internal/GraphTypes.h @@ -1,8 +1,8 @@ #pragma once +#include #include #include -#include #include #include @@ -19,95 +19,95 @@ using TickType_t = uint32_t; #include "ESPWorker.h" enum class LifecycleState : uint8_t { - Idle, - Initializing, - Running, - Reinitializing, - Deinitializing, - Failed, + Idle, + Initializing, + Running, + Reinitializing, + Deinitializing, + Failed, }; enum class LifecycleSectionMode : uint8_t { - Blocking, - Deferred, + Blocking, + Deferred, }; enum class LifecycleErrorCode : uint8_t { - None, - DuplicateNode, - MissingDependency, - CycleDetected, - Busy, - InitFailed, - TeardownFailed, - InvalidSection, - InvalidConfig, - UnknownNode, - NodeResolutionFailed, + None, + DuplicateNode, + MissingDependency, + CycleDetected, + Busy, + InitFailed, + TeardownFailed, + InvalidSection, + InvalidConfig, + UnknownNode, + NodeResolutionFailed, }; enum class LifecycleLogLevel : uint8_t { - Debug, - Info, - Warn, - Error, + Debug, + Info, + Warn, + Error, }; struct LifecycleResult { - bool ok = true; - LifecycleErrorCode code = LifecycleErrorCode::None; - const char* nodeName = nullptr; - const char* detail = nullptr; + bool ok = true; + LifecycleErrorCode code = LifecycleErrorCode::None; + const char *nodeName = nullptr; + const char *detail = nullptr; }; struct LifecycleSnapshot { - LifecycleState state = LifecycleState::Idle; - const char* activeNode = nullptr; - uint16_t completed = 0; - uint16_t total = 0; - bool failed = false; - LifecycleErrorCode errorCode = LifecycleErrorCode::None; - uint32_t updatedAtMs = 0; + LifecycleState state = LifecycleState::Idle; + const char *activeNode = nullptr; + uint16_t completed = 0; + uint16_t total = 0; + bool failed = false; + LifecycleErrorCode errorCode = LifecycleErrorCode::None; + uint32_t updatedAtMs = 0; }; struct LifecycleConfig { - ESPWorker* worker = nullptr; - uint32_t defaultStepTimeoutMs = 3000; - TickType_t waitTicks = pdMS_TO_TICKS(500); - const char* workerName = "lifecycle-flow"; - size_t workerStackSizeBytes = 6 * 1024; - bool enableParallelInit = false; - bool enableParallelDeinit = false; - bool enableParallelReinit = false; - bool dependencyReinitialization = false; - bool rollbackOnInitFailure = true; - bool continueTeardownOnFailure = false; - uint16_t maxNodes = 64; - uint16_t maxDependencies = 256; - std::function onInitStarted; - std::function onReady; - std::function onInitFailed; - std::function onSnapshot; - std::function logger; + ESPWorker *worker = nullptr; + uint32_t defaultStepTimeoutMs = 3000; + TickType_t waitTicks = pdMS_TO_TICKS(500); + const char *workerName = "lifecycle-flow"; + size_t workerStackSizeBytes = 6 * 1024; + bool enableParallelInit = false; + bool enableParallelDeinit = false; + bool enableParallelReinit = false; + bool dependencyReinitialization = false; + bool rollbackOnInitFailure = true; + bool continueTeardownOnFailure = false; + uint16_t maxNodes = 64; + uint16_t maxDependencies = 256; + std::function onInitStarted; + std::function onReady; + std::function onInitFailed; + std::function onSnapshot; + std::function logger; }; struct LifecycleSectionDefinition { - std::string name; - LifecycleSectionMode mode = LifecycleSectionMode::Blocking; - std::function readinessCheck; - std::function waitFn; + std::string name; + LifecycleSectionMode mode = LifecycleSectionMode::Blocking; + std::function readinessCheck; + std::function waitFn; }; struct LifecycleNodeDefinition { - std::string name; - size_t sectionIndex = 0; - std::function initFn; - std::function teardownFn; - uint32_t timeoutMs = 0; - bool optional = false; - bool parallelSafe = false; - std::vector dependenciesByName; - std::vector dependentsByName; - std::vector dependencyIndexes; - std::vector reverseDependencyIndexes; + std::string name; + size_t sectionIndex = 0; + std::function initFn; + std::function teardownFn; + uint32_t timeoutMs = 0; + bool optional = false; + bool parallelSafe = false; + std::vector dependenciesByName; + std::vector dependentsByName; + std::vector dependencyIndexes; + std::vector reverseDependencyIndexes; }; diff --git a/src/reload.cpp b/src/reload.cpp index 7d4ee01..76c5cb0 100644 --- a/src/reload.cpp +++ b/src/reload.cpp @@ -22,173 +22,169 @@ #endif bool ESPLifecycle::startReloadListener( - ESPEventBus& eventBus, + ESPEventBus &eventBus, uint16_t eventId, - std::function(void*)> payloadToNodeNames + std::function(void *)> payloadToNodeNames ) { - if( payloadToNodeNames == nullptr ){ - return false; - } - - if( config.worker == nullptr ){ - log(LifecycleLogLevel::Error, "reload listener requires config.worker"); - return false; - } - - stopReloadListener(); - - EventBusSub subId = eventBus.subscribe( - eventId, - [this, payloadToNodeNames](void* payload, void* /*userArg*/) { - const std::vector names = payloadToNodeNames(payload); - if( names.empty() ){ - return; - } - scheduleNodeReinitialize(names); - } - ); - - if( subId == 0 ){ - return false; - } - - std::lock_guard lock(listenerMutex); - listenerBus = &eventBus; - listenerEventId = eventId; - listenerPayloadNamesFn = std::move(payloadToNodeNames); - listenerSubId = subId; - pendingNodeNames.clear(); - listenerWorkerRunning = false; - return true; + if (payloadToNodeNames == nullptr) { + return false; + } + + if (config.worker == nullptr) { + log(LifecycleLogLevel::Error, "reload listener requires config.worker"); + return false; + } + + stopReloadListener(); + + EventBusSub subId = + eventBus.subscribe(eventId, [this, payloadToNodeNames](void *payload, void * /*userArg*/) { + const std::vector names = payloadToNodeNames(payload); + if (names.empty()) { + return; + } + scheduleNodeReinitialize(names); + }); + + if (subId == 0) { + return false; + } + + std::lock_guard lock(listenerMutex); + listenerBus = &eventBus; + listenerEventId = eventId; + listenerPayloadNamesFn = std::move(payloadToNodeNames); + listenerSubId = subId; + pendingNodeNames.clear(); + listenerWorkerRunning = false; + return true; } void ESPLifecycle::stopReloadListener() { - std::lock_guard lock(listenerMutex); - if( listenerBus != nullptr && listenerSubId != 0 ){ - listenerBus->unsubscribe(listenerSubId); - } - - listenerBus = nullptr; - listenerSubId = 0; - listenerEventId = 0; - listenerPayloadNamesFn = {}; - pendingNodeNames.clear(); - listenerWorkerRunning = false; + std::lock_guard lock(listenerMutex); + if (listenerBus != nullptr && listenerSubId != 0) { + listenerBus->unsubscribe(listenerSubId); + } + + listenerBus = nullptr; + listenerSubId = 0; + listenerEventId = 0; + listenerPayloadNamesFn = {}; + pendingNodeNames.clear(); + listenerWorkerRunning = false; } -void ESPLifecycle::scheduleNodeReinitialize(const std::vector& nodeNames) { - if( nodeNames.empty() ){ - return; - } - - bool shouldSpawnWorker = false; - - { - std::lock_guard lock(listenerMutex); - for( const char* name : nodeNames ){ - if( name == nullptr || name[0] == '\0' ){ - continue; - } - - if( std::find(pendingNodeNames.begin(), pendingNodeNames.end(), name) == pendingNodeNames.end() ){ - pendingNodeNames.emplace_back(name); - } - } - - if( pendingNodeNames.empty() ){ - return; - } - - if( !listenerWorkerRunning ){ - listenerWorkerRunning = true; - shouldSpawnWorker = true; - } - } - - if( !shouldSpawnWorker ){ - return; - } - - if( config.worker == nullptr ){ - std::lock_guard lock(listenerMutex); - listenerWorkerRunning = false; - return; - } - - WorkerConfig workerConfig{}; - workerConfig.stackSizeBytes = config.workerStackSizeBytes; - if( config.workerName != nullptr ){ - workerConfig.name = std::string(config.workerName) + "-reload"; - } else { - workerConfig.name = "lifecycle-reload"; - } - - WorkerResult spawnResult = config.worker->spawn( - [this]() { - listenerWorkerLoop(); - }, - workerConfig - ); - - if( spawnResult.error != WorkerError::None ){ - std::lock_guard lock(listenerMutex); - listenerWorkerRunning = false; - log(LifecycleLogLevel::Error, "failed to spawn reload listener worker"); - } +void ESPLifecycle::scheduleNodeReinitialize(const std::vector &nodeNames) { + if (nodeNames.empty()) { + return; + } + + bool shouldSpawnWorker = false; + + { + std::lock_guard lock(listenerMutex); + for (const char *name : nodeNames) { + if (name == nullptr || name[0] == '\0') { + continue; + } + + if (std::find(pendingNodeNames.begin(), pendingNodeNames.end(), name) == + pendingNodeNames.end()) { + pendingNodeNames.emplace_back(name); + } + } + + if (pendingNodeNames.empty()) { + return; + } + + if (!listenerWorkerRunning) { + listenerWorkerRunning = true; + shouldSpawnWorker = true; + } + } + + if (!shouldSpawnWorker) { + return; + } + + if (config.worker == nullptr) { + std::lock_guard lock(listenerMutex); + listenerWorkerRunning = false; + return; + } + + WorkerConfig workerConfig{}; + workerConfig.stackSizeBytes = config.workerStackSizeBytes; + if (config.workerName != nullptr) { + workerConfig.name = std::string(config.workerName) + "-reload"; + } else { + workerConfig.name = "lifecycle-reload"; + } + + WorkerResult spawnResult = + config.worker->spawn([this]() { listenerWorkerLoop(); }, workerConfig); + + if (spawnResult.error != WorkerError::None) { + std::lock_guard lock(listenerMutex); + listenerWorkerRunning = false; + log(LifecycleLogLevel::Error, "failed to spawn reload listener worker"); + } } void ESPLifecycle::listenerWorkerLoop() { - while( true ){ + while (true) { #if __has_include() - vTaskDelay(pdMS_TO_TICKS(listenerCoalesceMs)); + vTaskDelay(pdMS_TO_TICKS(listenerCoalesceMs)); #else - std::this_thread::sleep_for(std::chrono::milliseconds(listenerCoalesceMs)); + std::this_thread::sleep_for(std::chrono::milliseconds(listenerCoalesceMs)); #endif - std::vector pending; - { - std::lock_guard lock(listenerMutex); - pending.swap(pendingNodeNames); - } - - if( pending.empty() ){ - break; - } - - std::vector names; - names.reserve(pending.size()); - for( const std::string& name : pending ){ - names.push_back(name.c_str()); - } - - LifecycleResult result = reinitialize(names); - if( !result.ok ){ - if( result.code == LifecycleErrorCode::Busy ){ - log(LifecycleLogLevel::Warn, "reload reinitialize rejected because lifecycle is busy"); - } else { - log(LifecycleLogLevel::Error, "reload reinitialize failed"); - } - } - } - - { - std::lock_guard lock(listenerMutex); - listenerWorkerRunning = false; - - if( !pendingNodeNames.empty() ){ - listenerWorkerRunning = true; - if( config.worker != nullptr ){ - WorkerConfig workerConfig{}; - workerConfig.stackSizeBytes = config.workerStackSizeBytes; - if( config.workerName != nullptr ){ - workerConfig.name = std::string(config.workerName) + "-reload"; - } else { - workerConfig.name = "lifecycle-reload"; - } - (void)config.worker->spawn([this]() { listenerWorkerLoop(); }, workerConfig); - } else { - listenerWorkerRunning = false; - } - } - } + std::vector pending; + { + std::lock_guard lock(listenerMutex); + pending.swap(pendingNodeNames); + } + + if (pending.empty()) { + break; + } + + std::vector names; + names.reserve(pending.size()); + for (const std::string &name : pending) { + names.push_back(name.c_str()); + } + + LifecycleResult result = reinitialize(names); + if (!result.ok) { + if (result.code == LifecycleErrorCode::Busy) { + log(LifecycleLogLevel::Warn, + "reload reinitialize rejected because lifecycle is busy"); + } else { + log(LifecycleLogLevel::Error, "reload reinitialize failed"); + } + } + } + + { + std::lock_guard lock(listenerMutex); + listenerWorkerRunning = false; + + if (!pendingNodeNames.empty()) { + listenerWorkerRunning = true; + if (config.worker != nullptr) { + WorkerConfig workerConfig{}; + workerConfig.stackSizeBytes = config.workerStackSizeBytes; + if (config.workerName != nullptr) { + workerConfig.name = std::string(config.workerName) + "-reload"; + } else { + workerConfig.name = "lifecycle-reload"; + } + (void)config.worker->spawn([this]() { listenerWorkerLoop(); }, workerConfig); + } else { + listenerWorkerRunning = false; + } + } + } } diff --git a/src/runtime.cpp b/src/runtime.cpp index 49a0625..c67cebc 100644 --- a/src/runtime.cpp +++ b/src/runtime.cpp @@ -21,889 +21,941 @@ namespace { -bool anyInitializedNode(const std::vector& initialized) { - for( bool value : initialized ){ - if( value ){ - return true; - } - } - return false; +bool anyInitializedNode(const std::vector &initialized) { + for (bool value : initialized) { + if (value) { + return true; + } + } + return false; } -} // namespace +} // namespace LifecycleResult ESPLifecycle::initialize() { - std::unique_lock lock(transitionMutex, std::try_to_lock); - if( !lock.owns_lock() ){ - return failResult(LifecycleErrorCode::Busy, nullptr, "lifecycle is busy", false); - } - - if( !graphBuilt ){ - LifecycleResult buildResult = validateAndBuildGraph(); - if( !buildResult.ok ){ - return buildResult; - } - } - - if( state() == LifecycleState::Running ){ - return okResult("already running"); - } - - setPhase("initialize"); - - if( config.onInitStarted ){ - config.onInitStarted(); - } - - LifecycleResult initResult = initializeInternal( - allNodeIndexes(), - LifecycleState::Initializing, - config.enableParallelInit - ); - if( !initResult.ok ){ - if( config.onInitFailed ){ - config.onInitFailed(); - } - return initResult; - } - - setState(LifecycleState::Running, nullptr); - setPhase("idle"); - if( config.onReady ){ - config.onReady(); - } - - return okResult("initialized"); + std::unique_lock lock(transitionMutex, std::try_to_lock); + if (!lock.owns_lock()) { + return failResult(LifecycleErrorCode::Busy, nullptr, "lifecycle is busy", false); + } + + if (!graphBuilt) { + LifecycleResult buildResult = validateAndBuildGraph(); + if (!buildResult.ok) { + return buildResult; + } + } + + if (state() == LifecycleState::Running) { + return okResult("already running"); + } + + setPhase("initialize"); + + if (config.onInitStarted) { + config.onInitStarted(); + } + + LifecycleResult initResult = initializeInternal( + allNodeIndexes(), + LifecycleState::Initializing, + config.enableParallelInit + ); + if (!initResult.ok) { + if (config.onInitFailed) { + config.onInitFailed(); + } + return initResult; + } + + setState(LifecycleState::Running, nullptr); + setPhase("idle"); + if (config.onReady) { + config.onReady(); + } + + return okResult("initialized"); } LifecycleResult ESPLifecycle::deinitialize() { - std::unique_lock lock(transitionMutex, std::try_to_lock); - if( !lock.owns_lock() ){ - return failResult(LifecycleErrorCode::Busy, nullptr, "lifecycle is busy", false); - } - - if( !anyInitializedNode(initialized) ){ - setState(LifecycleState::Idle, nullptr); - setPhase("idle"); - return okResult("deinitialize no-op"); - } - - setPhase("deinitialize"); - LifecycleResult result = deinitializeInternal( - allNodeIndexes(), - true, - config.enableParallelDeinit - ); - if( !result.ok ){ - setState(LifecycleState::Failed, result.nodeName); - return result; - } - - setState(LifecycleState::Idle, nullptr); - setPhase("idle"); - return okResult("deinitialized"); + std::unique_lock lock(transitionMutex, std::try_to_lock); + if (!lock.owns_lock()) { + return failResult(LifecycleErrorCode::Busy, nullptr, "lifecycle is busy", false); + } + + if (!anyInitializedNode(initialized)) { + setState(LifecycleState::Idle, nullptr); + setPhase("idle"); + return okResult("deinitialize no-op"); + } + + setPhase("deinitialize"); + LifecycleResult result = + deinitializeInternal(allNodeIndexes(), true, config.enableParallelDeinit); + if (!result.ok) { + setState(LifecycleState::Failed, result.nodeName); + return result; + } + + setState(LifecycleState::Idle, nullptr); + setPhase("idle"); + return okResult("deinitialized"); } -LifecycleResult ESPLifecycle::deinitialize(const std::vector& nodeNames) { - std::unique_lock lock(transitionMutex, std::try_to_lock); - if( !lock.owns_lock() ){ - return failResult(LifecycleErrorCode::Busy, nullptr, "lifecycle is busy", false); - } - - if( !graphBuilt ){ - LifecycleResult buildResult = validateAndBuildGraph(); - if( !buildResult.ok ){ - return buildResult; - } - } - - std::vector subset; - LifecycleResult resolveResult = resolveNodeNamesToSubset(nodeNames, subset); - if( !resolveResult.ok ){ - return resolveResult; - } - - LifecycleResult closureResult = expandSubsetWithDependents(subset); - if( !closureResult.ok ){ - return closureResult; - } - - setPhase("deinitialize"); - LifecycleResult result = deinitializeInternal(subset, true, config.enableParallelDeinit); - if( !result.ok ){ - setState(LifecycleState::Failed, result.nodeName); - return result; - } - - if( anyInitializedNode(initialized) ){ - setState(LifecycleState::Running, nullptr); - } else { - setState(LifecycleState::Idle, nullptr); - } - setPhase("idle"); - return okResult("deinitialized nodes"); +LifecycleResult ESPLifecycle::deinitialize(const std::vector &nodeNames) { + std::unique_lock lock(transitionMutex, std::try_to_lock); + if (!lock.owns_lock()) { + return failResult(LifecycleErrorCode::Busy, nullptr, "lifecycle is busy", false); + } + + if (!graphBuilt) { + LifecycleResult buildResult = validateAndBuildGraph(); + if (!buildResult.ok) { + return buildResult; + } + } + + std::vector subset; + LifecycleResult resolveResult = resolveNodeNamesToSubset(nodeNames, subset); + if (!resolveResult.ok) { + return resolveResult; + } + + LifecycleResult closureResult = expandSubsetWithDependents(subset); + if (!closureResult.ok) { + return closureResult; + } + + setPhase("deinitialize"); + LifecycleResult result = deinitializeInternal(subset, true, config.enableParallelDeinit); + if (!result.ok) { + setState(LifecycleState::Failed, result.nodeName); + return result; + } + + if (anyInitializedNode(initialized)) { + setState(LifecycleState::Running, nullptr); + } else { + setState(LifecycleState::Idle, nullptr); + } + setPhase("idle"); + return okResult("deinitialized nodes"); } LifecycleResult ESPLifecycle::reinitializeAll() { - std::unique_lock lock(transitionMutex, std::try_to_lock); - if( !lock.owns_lock() ){ - return failResult(LifecycleErrorCode::Busy, nullptr, "lifecycle is busy", false); - } - - if( !graphBuilt ){ - LifecycleResult buildResult = validateAndBuildGraph(); - if( !buildResult.ok ){ - return buildResult; - } - } - - setPhase("reinitialize"); - setState(LifecycleState::Reinitializing, nullptr); - - std::vector subset = allNodeIndexes(); - LifecycleResult deinitResult = deinitializeInternal( - subset, - false, - config.enableParallelReinit - ); - if( !deinitResult.ok ){ - setState(LifecycleState::Failed, deinitResult.nodeName); - return deinitResult; - } - - LifecycleResult initResult = initializeInternal( - subset, - LifecycleState::Reinitializing, - config.enableParallelReinit - ); - if( !initResult.ok ){ - if( config.onInitFailed ){ - config.onInitFailed(); - } - return initResult; - } - - setState(LifecycleState::Running, nullptr); - setPhase("idle"); - if( config.onReady ){ - config.onReady(); - } - - return okResult("reinitialized all"); + std::unique_lock lock(transitionMutex, std::try_to_lock); + if (!lock.owns_lock()) { + return failResult(LifecycleErrorCode::Busy, nullptr, "lifecycle is busy", false); + } + + if (!graphBuilt) { + LifecycleResult buildResult = validateAndBuildGraph(); + if (!buildResult.ok) { + return buildResult; + } + } + + setPhase("reinitialize"); + setState(LifecycleState::Reinitializing, nullptr); + + std::vector subset = allNodeIndexes(); + LifecycleResult deinitResult = deinitializeInternal(subset, false, config.enableParallelReinit); + if (!deinitResult.ok) { + setState(LifecycleState::Failed, deinitResult.nodeName); + return deinitResult; + } + + LifecycleResult initResult = + initializeInternal(subset, LifecycleState::Reinitializing, config.enableParallelReinit); + if (!initResult.ok) { + if (config.onInitFailed) { + config.onInitFailed(); + } + return initResult; + } + + setState(LifecycleState::Running, nullptr); + setPhase("idle"); + if (config.onReady) { + config.onReady(); + } + + return okResult("reinitialized all"); } -LifecycleResult ESPLifecycle::reinitialize(const std::vector& nodeNames) { - std::unique_lock lock(transitionMutex, std::try_to_lock); - if( !lock.owns_lock() ){ - return failResult(LifecycleErrorCode::Busy, nullptr, "lifecycle is busy", false); - } - - if( !graphBuilt ){ - LifecycleResult buildResult = validateAndBuildGraph(); - if( !buildResult.ok ){ - return buildResult; - } - } - - std::vector subset; - LifecycleResult resolveResult = resolveNodeNamesToSubset(nodeNames, subset); - if( !resolveResult.ok ){ - return resolveResult; - } - - if( config.dependencyReinitialization ){ - LifecycleResult closureResult = expandSubsetForReinitialize(subset); - if( !closureResult.ok ){ - return closureResult; - } - } - - setPhase("reinitialize"); - setState(LifecycleState::Reinitializing, nullptr); - - LifecycleResult deinitResult = deinitializeInternal( - subset, - false, - config.enableParallelReinit - ); - if( !deinitResult.ok ){ - setState(LifecycleState::Failed, deinitResult.nodeName); - return deinitResult; - } - - LifecycleResult initResult = initializeInternal( - subset, - LifecycleState::Reinitializing, - config.enableParallelReinit - ); - if( !initResult.ok ){ - if( config.onInitFailed ){ - config.onInitFailed(); - } - return initResult; - } - - setState(LifecycleState::Running, nullptr); - setPhase("idle"); - return okResult("reinitialized nodes"); +LifecycleResult ESPLifecycle::reinitialize(const std::vector &nodeNames) { + std::unique_lock lock(transitionMutex, std::try_to_lock); + if (!lock.owns_lock()) { + return failResult(LifecycleErrorCode::Busy, nullptr, "lifecycle is busy", false); + } + + if (!graphBuilt) { + LifecycleResult buildResult = validateAndBuildGraph(); + if (!buildResult.ok) { + return buildResult; + } + } + + std::vector subset; + LifecycleResult resolveResult = resolveNodeNamesToSubset(nodeNames, subset); + if (!resolveResult.ok) { + return resolveResult; + } + + if (config.dependencyReinitialization) { + LifecycleResult closureResult = expandSubsetForReinitialize(subset); + if (!closureResult.ok) { + return closureResult; + } + } + + setPhase("reinitialize"); + setState(LifecycleState::Reinitializing, nullptr); + + LifecycleResult deinitResult = deinitializeInternal(subset, false, config.enableParallelReinit); + if (!deinitResult.ok) { + setState(LifecycleState::Failed, deinitResult.nodeName); + return deinitResult; + } + + LifecycleResult initResult = + initializeInternal(subset, LifecycleState::Reinitializing, config.enableParallelReinit); + if (!initResult.ok) { + if (config.onInitFailed) { + config.onInitFailed(); + } + return initResult; + } + + setState(LifecycleState::Running, nullptr); + setPhase("idle"); + return okResult("reinitialized nodes"); } LifecycleResult ESPLifecycle::initializeInternal( - const std::vector& subset, - LifecycleState transitionState, - bool enableParallel + const std::vector &subset, LifecycleState transitionState, bool enableParallel ) { - setState(transitionState, nullptr); - - std::vector selected(nodes.size(), false); - for( size_t index : subset ){ - if( index < selected.size() ){ - selected[index] = true; - } - } - - for( size_t sectionIndex = 0; sectionIndex < sections.size(); sectionIndex++ ){ - LifecycleResult sectionResult = runSectionInitialize(sectionIndex, subset, enableParallel); - if( !sectionResult.ok ){ - if( config.rollbackOnInitFailure ){ - std::vector rollbackSubset; - rollbackSubset.reserve(nodes.size()); - for( size_t i = 0; i < nodes.size(); i++ ){ - if( initialized[i] && selected[i] ){ - rollbackSubset.push_back(i); - } - } - (void)deinitializeInternal( - rollbackSubset, - false, - config.enableParallelDeinit - ); - } - - return sectionResult; - } - } - - return okResult(); + setState(transitionState, nullptr); + + std::vector selected(nodes.size(), false); + for (size_t index : subset) { + if (index < selected.size()) { + selected[index] = true; + } + } + + for (size_t sectionIndex = 0; sectionIndex < sections.size(); sectionIndex++) { + LifecycleResult sectionResult = runSectionInitialize(sectionIndex, subset, enableParallel); + if (!sectionResult.ok) { + if (config.rollbackOnInitFailure) { + std::vector rollbackSubset; + rollbackSubset.reserve(nodes.size()); + for (size_t i = 0; i < nodes.size(); i++) { + if (initialized[i] && selected[i]) { + rollbackSubset.push_back(i); + } + } + (void)deinitializeInternal(rollbackSubset, false, config.enableParallelDeinit); + } + + return sectionResult; + } + } + + return okResult(); } LifecycleResult ESPLifecycle::runSectionInitialize( - size_t sectionIndex, - const std::vector& subset, - bool enableParallel + size_t sectionIndex, const std::vector &subset, bool enableParallel ) { - if( sectionIndex >= sections.size() ){ - return failResult(LifecycleErrorCode::InvalidSection, nullptr, "section index out of bounds", true); - } - - const LifecycleSectionDefinition& sectionDef = sections[sectionIndex]; - if( sectionDef.mode == LifecycleSectionMode::Deferred ){ - while( true ){ - if( sectionDef.readinessCheck && sectionDef.readinessCheck() ){ - break; - } - - if( !sectionDef.waitFn ){ - return failResult( - LifecycleErrorCode::InvalidSection, - nullptr, - "deferred section wait callback missing", - true - ); - } - - sectionDef.waitFn(config.waitTicks); - } - } - - std::vector sectionSubset; - sectionSubset.reserve(subset.size()); - for( size_t nodeIndex : subset ){ - if( nodeIndex < nodes.size() && nodes[nodeIndex].sectionIndex == sectionIndex ){ - sectionSubset.push_back(nodeIndex); - } - } - - if( sectionSubset.empty() ){ - return okResult(); - } - - std::vector> batches = buildWavesForSubset(sectionSubset, true); - if( !sectionSubset.empty() && batches.empty() ){ - return failResult(LifecycleErrorCode::CycleDetected, nullptr, "unable to build initialize waves", true); - } - return runPhaseBatches(batches, true, enableParallel); + if (sectionIndex >= sections.size()) { + return failResult( + LifecycleErrorCode::InvalidSection, + nullptr, + "section index out of bounds", + true + ); + } + + const LifecycleSectionDefinition §ionDef = sections[sectionIndex]; + if (sectionDef.mode == LifecycleSectionMode::Deferred) { + while (true) { + if (sectionDef.readinessCheck && sectionDef.readinessCheck()) { + break; + } + + if (!sectionDef.waitFn) { + return failResult( + LifecycleErrorCode::InvalidSection, + nullptr, + "deferred section wait callback missing", + true + ); + } + + sectionDef.waitFn(config.waitTicks); + } + } + + std::vector sectionSubset; + sectionSubset.reserve(subset.size()); + for (size_t nodeIndex : subset) { + if (nodeIndex < nodes.size() && nodes[nodeIndex].sectionIndex == sectionIndex) { + sectionSubset.push_back(nodeIndex); + } + } + + if (sectionSubset.empty()) { + return okResult(); + } + + std::vector> batches = buildWavesForSubset(sectionSubset, true); + if (!sectionSubset.empty() && batches.empty()) { + return failResult( + LifecycleErrorCode::CycleDetected, + nullptr, + "unable to build initialize waves", + true + ); + } + return runPhaseBatches(batches, true, enableParallel); } LifecycleResult ESPLifecycle::deinitializeInternal( - const std::vector& subset, - bool updateState, - bool enableParallel + const std::vector &subset, bool updateState, bool enableParallel ) { - if( updateState ){ - setState(LifecycleState::Deinitializing, nullptr); - } - - std::vector teardownSubset; - teardownSubset.reserve(subset.size()); - for( size_t nodeIndex : subset ){ - if( nodeIndex < initialized.size() && initialized[nodeIndex] ){ - teardownSubset.push_back(nodeIndex); - } - } - - if( teardownSubset.empty() ){ - return okResult(); - } - - std::vector> batches = buildWavesForSubset(teardownSubset, false); - if( !teardownSubset.empty() && batches.empty() ){ - return failResult(LifecycleErrorCode::CycleDetected, nullptr, "unable to build deinitialize waves", true); - } - return runPhaseBatches(batches, false, enableParallel); + if (updateState) { + setState(LifecycleState::Deinitializing, nullptr); + } + + std::vector teardownSubset; + teardownSubset.reserve(subset.size()); + for (size_t nodeIndex : subset) { + if (nodeIndex < initialized.size() && initialized[nodeIndex]) { + teardownSubset.push_back(nodeIndex); + } + } + + if (teardownSubset.empty()) { + return okResult(); + } + + std::vector> batches = buildWavesForSubset(teardownSubset, false); + if (!teardownSubset.empty() && batches.empty()) { + return failResult( + LifecycleErrorCode::CycleDetected, + nullptr, + "unable to build deinitialize waves", + true + ); + } + return runPhaseBatches(batches, false, enableParallel); } LifecycleResult ESPLifecycle::runPhaseBatches( - const std::vector>& batches, - bool initializePhase, - bool enableParallel + const std::vector> &batches, bool initializePhase, bool enableParallel ) { - for( const std::vector& batch : batches ){ - if( batch.empty() ){ - continue; - } - - if( !enableParallel ){ - for( size_t nodeIndex : batch ){ - LifecycleResult result = initializePhase - ? runNodeInit(nodeIndex, true) - : runNodeTeardown(nodeIndex, true); - - if( result.ok ){ - continue; - } - - if( initializePhase && nodes[nodeIndex].optional ){ - log(LifecycleLogLevel::Warn, "optional node init failed, continuing"); - continue; - } - - if( !initializePhase && config.continueTeardownOnFailure ){ - log(LifecycleLogLevel::Warn, "teardown failed, continuing due to policy"); - continue; - } - - return result; - } - continue; - } - - std::vector parallelEligible; - std::vector sequentialOnly; - parallelEligible.reserve(batch.size()); - sequentialOnly.reserve(batch.size()); - for( size_t nodeIndex : batch ){ - if( isParallelEligible(nodeIndex) ){ - parallelEligible.push_back(nodeIndex); - } else { - sequentialOnly.push_back(nodeIndex); - } - } - - if( parallelEligible.size() < 2 ){ - for( size_t nodeIndex : batch ){ - LifecycleResult result = initializePhase - ? runNodeInit(nodeIndex, true) - : runNodeTeardown(nodeIndex, true); - - if( result.ok ){ - continue; - } - - if( initializePhase && nodes[nodeIndex].optional ){ - log(LifecycleLogLevel::Warn, "optional node init failed, continuing"); - continue; - } - - if( !initializePhase && config.continueTeardownOnFailure ){ - log(LifecycleLogLevel::Warn, "teardown failed, continuing due to policy"); - continue; - } - - return result; - } - continue; - } - - LifecycleResult parallelResult = runParallelBatch(parallelEligible, initializePhase); - if( !parallelResult.ok ){ - if( initializePhase && parallelResult.nodeName != nullptr ){ - auto it = std::find_if(nodes.begin(), nodes.end(), [&](const LifecycleNodeDefinition& node) { - return node.name == parallelResult.nodeName; - }); - if( it != nodes.end() && it->optional ){ - log(LifecycleLogLevel::Warn, "optional node init failed in parallel batch, continuing"); - continue; - } - } - - if( !initializePhase && config.continueTeardownOnFailure ){ - log(LifecycleLogLevel::Warn, "parallel teardown failed, continuing due to policy"); - continue; - } - - return parallelResult; - } - - for( size_t nodeIndex : sequentialOnly ){ - LifecycleResult result = initializePhase - ? runNodeInit(nodeIndex, true) - : runNodeTeardown(nodeIndex, true); - - if( result.ok ){ - continue; - } - - if( initializePhase && nodes[nodeIndex].optional ){ - log(LifecycleLogLevel::Warn, "optional node init failed, continuing"); - continue; - } - - if( !initializePhase && config.continueTeardownOnFailure ){ - log(LifecycleLogLevel::Warn, "teardown failed, continuing due to policy"); - continue; - } - - return result; - } - } - - return okResult(); + for (const std::vector &batch : batches) { + if (batch.empty()) { + continue; + } + + if (!enableParallel) { + for (size_t nodeIndex : batch) { + LifecycleResult result = initializePhase ? runNodeInit(nodeIndex, true) + : runNodeTeardown(nodeIndex, true); + + if (result.ok) { + continue; + } + + if (initializePhase && nodes[nodeIndex].optional) { + log(LifecycleLogLevel::Warn, "optional node init failed, continuing"); + continue; + } + + if (!initializePhase && config.continueTeardownOnFailure) { + log(LifecycleLogLevel::Warn, "teardown failed, continuing due to policy"); + continue; + } + + return result; + } + continue; + } + + std::vector parallelEligible; + std::vector sequentialOnly; + parallelEligible.reserve(batch.size()); + sequentialOnly.reserve(batch.size()); + for (size_t nodeIndex : batch) { + if (isParallelEligible(nodeIndex)) { + parallelEligible.push_back(nodeIndex); + } else { + sequentialOnly.push_back(nodeIndex); + } + } + + if (parallelEligible.size() < 2) { + for (size_t nodeIndex : batch) { + LifecycleResult result = initializePhase ? runNodeInit(nodeIndex, true) + : runNodeTeardown(nodeIndex, true); + + if (result.ok) { + continue; + } + + if (initializePhase && nodes[nodeIndex].optional) { + log(LifecycleLogLevel::Warn, "optional node init failed, continuing"); + continue; + } + + if (!initializePhase && config.continueTeardownOnFailure) { + log(LifecycleLogLevel::Warn, "teardown failed, continuing due to policy"); + continue; + } + + return result; + } + continue; + } + + LifecycleResult parallelResult = runParallelBatch(parallelEligible, initializePhase); + if (!parallelResult.ok) { + if (initializePhase && parallelResult.nodeName != nullptr) { + auto it = std::find_if( + nodes.begin(), + nodes.end(), + [&](const LifecycleNodeDefinition &node) { + return node.name == parallelResult.nodeName; + } + ); + if (it != nodes.end() && it->optional) { + log(LifecycleLogLevel::Warn, + "optional node init failed in parallel batch, continuing"); + continue; + } + } + + if (!initializePhase && config.continueTeardownOnFailure) { + log(LifecycleLogLevel::Warn, "parallel teardown failed, continuing due to policy"); + continue; + } + + return parallelResult; + } + + for (size_t nodeIndex : sequentialOnly) { + LifecycleResult result = + initializePhase ? runNodeInit(nodeIndex, true) : runNodeTeardown(nodeIndex, true); + + if (result.ok) { + continue; + } + + if (initializePhase && nodes[nodeIndex].optional) { + log(LifecycleLogLevel::Warn, "optional node init failed, continuing"); + continue; + } + + if (!initializePhase && config.continueTeardownOnFailure) { + log(LifecycleLogLevel::Warn, "teardown failed, continuing due to policy"); + continue; + } + + return result; + } + } + + return okResult(); } -LifecycleResult ESPLifecycle::runParallelBatch(const std::vector& batch, bool initializePhase) { - if( config.worker == nullptr ){ - return failResult( - LifecycleErrorCode::InvalidConfig, - nullptr, - "parallel execution requires config.worker", - true - ); - } - - struct BatchResult { - size_t nodeIndex = 0; - bool ok = false; - }; - - std::vector> handlers; - handlers.reserve(batch.size()); - - std::mutex resultMutex; - std::vector results; - results.reserve(batch.size()); - - for( size_t nodeIndex : batch ){ - if( nodeIndex >= nodes.size() ){ - return failResult(LifecycleErrorCode::InvalidConfig, nullptr, "node index out of bounds", true); - } - - WorkerConfig workerConfig{}; - workerConfig.stackSizeBytes = config.workerStackSizeBytes; - if( config.workerName != nullptr ){ - workerConfig.name = std::string(config.workerName) + "-parallel"; - } else { - workerConfig.name = "lifecycle-parallel"; - } - - setState(state(), nodes[nodeIndex].name.c_str()); - - WorkerResult workerResult = config.worker->spawn( - [this, nodeIndex, initializePhase, &resultMutex, &results]() { - bool ok = false; - if( initializePhase ){ - if( nodes[nodeIndex].initFn ){ - ok = nodes[nodeIndex].initFn(); - } - } else { - if( nodes[nodeIndex].teardownFn ){ - ok = nodes[nodeIndex].teardownFn(); - } - } - - std::lock_guard lock(resultMutex); - results.push_back(BatchResult{nodeIndex, ok}); - }, - workerConfig - ); - - if( workerResult.error != WorkerError::None || workerResult.handler == nullptr ){ - return failResult( - LifecycleErrorCode::InvalidConfig, - nodes[nodeIndex].name.c_str(), - "failed to start parallel worker", - true - ); - } - - handlers.push_back(workerResult.handler); - } - - for( const auto& handler : handlers ){ - if( handler == nullptr ){ - continue; - } - - const bool workerStopped = handler->wait(pdMS_TO_TICKS(1000)); - if( !workerStopped ){ - handler->destroy(); - handler->wait(pdMS_TO_TICKS(250)); - } - } - - std::vector collected; - { - std::lock_guard lock(resultMutex); - collected = results; - } - - for( size_t nodeIndex : batch ){ - auto it = std::find_if(collected.begin(), collected.end(), [&](const BatchResult& result) { - return result.nodeIndex == nodeIndex; - }); - - if( it == collected.end() ){ - return failResult( - LifecycleErrorCode::InvalidConfig, - nodes[nodeIndex].name.c_str(), - "parallel batch missing node result", - true - ); - } - - if( it->ok ){ - initialized[nodeIndex] = initializePhase; - markProgress(nodes[nodeIndex].name.c_str(), true); - continue; - } - - if( initializePhase && nodes[nodeIndex].optional ){ - log(LifecycleLogLevel::Warn, "optional node init failed in parallel batch"); - continue; - } - - if( !initializePhase && config.continueTeardownOnFailure ){ - log(LifecycleLogLevel::Warn, "teardown failed in parallel batch, continuing due to policy"); - continue; - } - - return failResult( - initializePhase ? LifecycleErrorCode::InitFailed : LifecycleErrorCode::TeardownFailed, - nodes[nodeIndex].name.c_str(), - initializePhase ? "node init failed" : "node teardown failed", - true - ); - } - - return okResult(); +LifecycleResult +ESPLifecycle::runParallelBatch(const std::vector &batch, bool initializePhase) { + if (config.worker == nullptr) { + return failResult( + LifecycleErrorCode::InvalidConfig, + nullptr, + "parallel execution requires config.worker", + true + ); + } + + struct BatchResult { + size_t nodeIndex = 0; + bool ok = false; + }; + + std::vector> handlers; + handlers.reserve(batch.size()); + + std::mutex resultMutex; + std::vector results; + results.reserve(batch.size()); + + for (size_t nodeIndex : batch) { + if (nodeIndex >= nodes.size()) { + return failResult( + LifecycleErrorCode::InvalidConfig, + nullptr, + "node index out of bounds", + true + ); + } + + WorkerConfig workerConfig{}; + workerConfig.stackSizeBytes = config.workerStackSizeBytes; + if (config.workerName != nullptr) { + workerConfig.name = std::string(config.workerName) + "-parallel"; + } else { + workerConfig.name = "lifecycle-parallel"; + } + + setState(state(), nodes[nodeIndex].name.c_str()); + + WorkerResult workerResult = config.worker->spawn( + [this, nodeIndex, initializePhase, &resultMutex, &results]() { + bool ok = false; + if (initializePhase) { + if (nodes[nodeIndex].initFn) { + ok = nodes[nodeIndex].initFn(); + } + } else { + if (nodes[nodeIndex].teardownFn) { + ok = nodes[nodeIndex].teardownFn(); + } + } + + std::lock_guard lock(resultMutex); + results.push_back(BatchResult{nodeIndex, ok}); + }, + workerConfig + ); + + if (workerResult.error != WorkerError::None || workerResult.handler == nullptr) { + return failResult( + LifecycleErrorCode::InvalidConfig, + nodes[nodeIndex].name.c_str(), + "failed to start parallel worker", + true + ); + } + + handlers.push_back(workerResult.handler); + } + + for (const auto &handler : handlers) { + if (handler == nullptr) { + continue; + } + + const bool workerStopped = handler->wait(pdMS_TO_TICKS(1000)); + if (!workerStopped) { + handler->destroy(); + handler->wait(pdMS_TO_TICKS(250)); + } + } + + std::vector collected; + { + std::lock_guard lock(resultMutex); + collected = results; + } + + for (size_t nodeIndex : batch) { + auto it = std::find_if(collected.begin(), collected.end(), [&](const BatchResult &result) { + return result.nodeIndex == nodeIndex; + }); + + if (it == collected.end()) { + return failResult( + LifecycleErrorCode::InvalidConfig, + nodes[nodeIndex].name.c_str(), + "parallel batch missing node result", + true + ); + } + + if (it->ok) { + initialized[nodeIndex] = initializePhase; + markProgress(nodes[nodeIndex].name.c_str(), true); + continue; + } + + if (initializePhase && nodes[nodeIndex].optional) { + log(LifecycleLogLevel::Warn, "optional node init failed in parallel batch"); + continue; + } + + if (!initializePhase && config.continueTeardownOnFailure) { + log(LifecycleLogLevel::Warn, + "teardown failed in parallel batch, continuing due to policy"); + continue; + } + + return failResult( + initializePhase ? LifecycleErrorCode::InitFailed : LifecycleErrorCode::TeardownFailed, + nodes[nodeIndex].name.c_str(), + initializePhase ? "node init failed" : "node teardown failed", + true + ); + } + + return okResult(); } LifecycleResult ESPLifecycle::runNodeInit(size_t nodeIndex, bool countProgress) { - if( nodeIndex >= nodes.size() ){ - return failResult(LifecycleErrorCode::InvalidConfig, nullptr, "node index out of bounds", true); - } - - const LifecycleNodeDefinition& node = nodes[nodeIndex]; - setState(state(), node.name.c_str()); - - if( !node.initFn ){ - return failResult(LifecycleErrorCode::InvalidConfig, node.name.c_str(), "init callback missing", true); - } - - if( !node.initFn() ){ - return failResult(LifecycleErrorCode::InitFailed, node.name.c_str(), "node init failed", true); - } - - initialized[nodeIndex] = true; - if( countProgress ){ - markProgress(node.name.c_str(), true); - } - return okResult(); + if (nodeIndex >= nodes.size()) { + return failResult( + LifecycleErrorCode::InvalidConfig, + nullptr, + "node index out of bounds", + true + ); + } + + const LifecycleNodeDefinition &node = nodes[nodeIndex]; + setState(state(), node.name.c_str()); + + if (!node.initFn) { + return failResult( + LifecycleErrorCode::InvalidConfig, + node.name.c_str(), + "init callback missing", + true + ); + } + + if (!node.initFn()) { + return failResult( + LifecycleErrorCode::InitFailed, + node.name.c_str(), + "node init failed", + true + ); + } + + initialized[nodeIndex] = true; + if (countProgress) { + markProgress(node.name.c_str(), true); + } + return okResult(); } LifecycleResult ESPLifecycle::runNodeTeardown(size_t nodeIndex, bool countProgress) { - if( nodeIndex >= nodes.size() ){ - return failResult(LifecycleErrorCode::InvalidConfig, nullptr, "node index out of bounds", true); - } - - const LifecycleNodeDefinition& node = nodes[nodeIndex]; - setState(state(), node.name.c_str()); - - if( !node.teardownFn ){ - return failResult(LifecycleErrorCode::InvalidConfig, node.name.c_str(), "teardown callback missing", true); - } - - if( !node.teardownFn() ){ - return failResult(LifecycleErrorCode::TeardownFailed, node.name.c_str(), "node teardown failed", true); - } - - initialized[nodeIndex] = false; - if( countProgress ){ - markProgress(node.name.c_str(), true); - } - return okResult(); + if (nodeIndex >= nodes.size()) { + return failResult( + LifecycleErrorCode::InvalidConfig, + nullptr, + "node index out of bounds", + true + ); + } + + const LifecycleNodeDefinition &node = nodes[nodeIndex]; + setState(state(), node.name.c_str()); + + if (!node.teardownFn) { + return failResult( + LifecycleErrorCode::InvalidConfig, + node.name.c_str(), + "teardown callback missing", + true + ); + } + + if (!node.teardownFn()) { + return failResult( + LifecycleErrorCode::TeardownFailed, + node.name.c_str(), + "node teardown failed", + true + ); + } + + initialized[nodeIndex] = false; + if (countProgress) { + markProgress(node.name.c_str(), true); + } + return okResult(); } -std::vector> ESPLifecycle::buildWavesForSubset( - const std::vector& subset, - bool initializePhase -) const { - std::vector> waves; - if( subset.empty() ){ - return waves; - } - - std::vector selected(nodes.size(), false); - for( size_t index : subset ){ - if( index < selected.size() ){ - selected[index] = true; - } - } - - std::vector indegree(nodes.size(), 0); - std::vector> outgoing(nodes.size()); - - for( size_t nodeIndex : subset ){ - if( nodeIndex >= nodes.size() ){ - continue; - } - - if( initializePhase ){ - for( size_t dep : nodes[nodeIndex].dependencyIndexes ){ - if( dep < selected.size() && selected[dep] ){ - indegree[nodeIndex]++; - outgoing[dep].push_back(nodeIndex); - } - } - } else { - for( size_t dependent : nodes[nodeIndex].reverseDependencyIndexes ){ - if( dependent < selected.size() && selected[dependent] ){ - indegree[nodeIndex]++; - outgoing[dependent].push_back(nodeIndex); - } - } - } - } - - std::unordered_map topoPos; - topoPos.reserve(topologicalOrder.size()); - for( size_t i = 0; i < topologicalOrder.size(); i++ ){ - topoPos[topologicalOrder[i]] = i; - } - - std::vector ready; - ready.reserve(subset.size()); - for( size_t nodeIndex : subset ){ - if( nodeIndex < indegree.size() && indegree[nodeIndex] == 0 ){ - ready.push_back(nodeIndex); - } - } - - auto waveOrder = [&](const size_t left, const size_t right) { - const size_t leftPos = topoPos.count(left) != 0 ? topoPos.at(left) : left; - const size_t rightPos = topoPos.count(right) != 0 ? topoPos.at(right) : right; - if( initializePhase ){ - return leftPos < rightPos; - } - return leftPos > rightPos; - }; - - size_t visitedCount = 0; - while( !ready.empty() ){ - std::sort(ready.begin(), ready.end(), waveOrder); - - std::vector wave = ready; - waves.push_back(wave); - ready.clear(); - - visitedCount += wave.size(); - - for( size_t nodeIndex : wave ){ - for( size_t target : outgoing[nodeIndex] ){ - if( target >= indegree.size() || indegree[target] == 0 ){ - continue; - } - - indegree[target]--; - if( indegree[target] == 0 ){ - ready.push_back(target); - } - } - } - } - - if( visitedCount != subset.size() ){ - return {}; - } - - return waves; +std::vector> +ESPLifecycle::buildWavesForSubset(const std::vector &subset, bool initializePhase) const { + std::vector> waves; + if (subset.empty()) { + return waves; + } + + std::vector selected(nodes.size(), false); + for (size_t index : subset) { + if (index < selected.size()) { + selected[index] = true; + } + } + + std::vector indegree(nodes.size(), 0); + std::vector> outgoing(nodes.size()); + + for (size_t nodeIndex : subset) { + if (nodeIndex >= nodes.size()) { + continue; + } + + if (initializePhase) { + for (size_t dep : nodes[nodeIndex].dependencyIndexes) { + if (dep < selected.size() && selected[dep]) { + indegree[nodeIndex]++; + outgoing[dep].push_back(nodeIndex); + } + } + } else { + for (size_t dependent : nodes[nodeIndex].reverseDependencyIndexes) { + if (dependent < selected.size() && selected[dependent]) { + indegree[nodeIndex]++; + outgoing[dependent].push_back(nodeIndex); + } + } + } + } + + std::unordered_map topoPos; + topoPos.reserve(topologicalOrder.size()); + for (size_t i = 0; i < topologicalOrder.size(); i++) { + topoPos[topologicalOrder[i]] = i; + } + + std::vector ready; + ready.reserve(subset.size()); + for (size_t nodeIndex : subset) { + if (nodeIndex < indegree.size() && indegree[nodeIndex] == 0) { + ready.push_back(nodeIndex); + } + } + + auto waveOrder = [&](const size_t left, const size_t right) { + const size_t leftPos = topoPos.count(left) != 0 ? topoPos.at(left) : left; + const size_t rightPos = topoPos.count(right) != 0 ? topoPos.at(right) : right; + if (initializePhase) { + return leftPos < rightPos; + } + return leftPos > rightPos; + }; + + size_t visitedCount = 0; + while (!ready.empty()) { + std::sort(ready.begin(), ready.end(), waveOrder); + + std::vector wave = ready; + waves.push_back(wave); + ready.clear(); + + visitedCount += wave.size(); + + for (size_t nodeIndex : wave) { + for (size_t target : outgoing[nodeIndex]) { + if (target >= indegree.size() || indegree[target] == 0) { + continue; + } + + indegree[target]--; + if (indegree[target] == 0) { + ready.push_back(target); + } + } + } + } + + if (visitedCount != subset.size()) { + return {}; + } + + return waves; } bool ESPLifecycle::isParallelEligible(size_t nodeIndex) const { - if( nodeIndex >= nodes.size() ){ - return false; - } + if (nodeIndex >= nodes.size()) { + return false; + } - const LifecycleNodeDefinition& node = nodes[nodeIndex]; - return node.parallelSafe || node.dependencyIndexes.empty(); + const LifecycleNodeDefinition &node = nodes[nodeIndex]; + return node.parallelSafe || node.dependencyIndexes.empty(); } -bool ESPLifecycle::requiresParallelWorkerForBatch(const std::vector& batch) const { - size_t eligible = 0; - for( size_t nodeIndex : batch ){ - if( isParallelEligible(nodeIndex) ){ - eligible++; - } - } - return eligible >= 2; +bool ESPLifecycle::requiresParallelWorkerForBatch(const std::vector &batch) const { + size_t eligible = 0; + for (size_t nodeIndex : batch) { + if (isParallelEligible(nodeIndex)) { + eligible++; + } + } + return eligible >= 2; } LifecycleResult ESPLifecycle::resolveNodeNamesToSubset( - const std::vector& nodeNames, - std::vector& outSubset + const std::vector &nodeNames, std::vector &outSubset ) { - std::unordered_map nodeByName; - nodeByName.reserve(nodes.size()); - for( size_t i = 0; i < nodes.size(); i++ ){ - nodeByName[nodes[i].name] = i; - } - - outSubset.clear(); - outSubset.reserve(nodeNames.size()); - - for( const char* nodeName : nodeNames ){ - if( nodeName == nullptr ){ - continue; - } - - auto it = nodeByName.find(nodeName); - if( it == nodeByName.end() ){ - return failResult(LifecycleErrorCode::UnknownNode, nodeName, "unknown node name", false); - } - - outSubset.push_back(it->second); - } - - std::sort(outSubset.begin(), outSubset.end()); - outSubset.erase(std::unique(outSubset.begin(), outSubset.end()), outSubset.end()); - - if( outSubset.empty() ){ - return failResult(LifecycleErrorCode::NodeResolutionFailed, nullptr, "empty node selection", false); - } - - return okResult(); + std::unordered_map nodeByName; + nodeByName.reserve(nodes.size()); + for (size_t i = 0; i < nodes.size(); i++) { + nodeByName[nodes[i].name] = i; + } + + outSubset.clear(); + outSubset.reserve(nodeNames.size()); + + for (const char *nodeName : nodeNames) { + if (nodeName == nullptr) { + continue; + } + + auto it = nodeByName.find(nodeName); + if (it == nodeByName.end()) { + return failResult( + LifecycleErrorCode::UnknownNode, + nodeName, + "unknown node name", + false + ); + } + + outSubset.push_back(it->second); + } + + std::sort(outSubset.begin(), outSubset.end()); + outSubset.erase(std::unique(outSubset.begin(), outSubset.end()), outSubset.end()); + + if (outSubset.empty()) { + return failResult( + LifecycleErrorCode::NodeResolutionFailed, + nullptr, + "empty node selection", + false + ); + } + + return okResult(); } -LifecycleResult ESPLifecycle::expandSubsetWithDependents(std::vector& subset) { - if( subset.empty() ){ - return okResult(); - } - - std::vector included(nodes.size(), false); - std::queue pending; - - for( size_t index : subset ){ - if( index >= nodes.size() ){ - return failResult(LifecycleErrorCode::NodeResolutionFailed, nullptr, "subset index out of bounds", false); - } - - if( !included[index] ){ - included[index] = true; - pending.push(index); - } - } - - while( !pending.empty() ){ - const size_t current = pending.front(); - pending.pop(); - - for( size_t dependent : nodes[current].reverseDependencyIndexes ){ - if( dependent < included.size() && !included[dependent] ){ - included[dependent] = true; - pending.push(dependent); - } - } - } - - subset.clear(); - for( size_t index : topologicalOrder ){ - if( index < included.size() && included[index] ){ - subset.push_back(index); - } - } - - if( subset.empty() ){ - return failResult(LifecycleErrorCode::NodeResolutionFailed, nullptr, "dependent closure is empty", false); - } - - return okResult(); +LifecycleResult ESPLifecycle::expandSubsetWithDependents(std::vector &subset) { + if (subset.empty()) { + return okResult(); + } + + std::vector included(nodes.size(), false); + std::queue pending; + + for (size_t index : subset) { + if (index >= nodes.size()) { + return failResult( + LifecycleErrorCode::NodeResolutionFailed, + nullptr, + "subset index out of bounds", + false + ); + } + + if (!included[index]) { + included[index] = true; + pending.push(index); + } + } + + while (!pending.empty()) { + const size_t current = pending.front(); + pending.pop(); + + for (size_t dependent : nodes[current].reverseDependencyIndexes) { + if (dependent < included.size() && !included[dependent]) { + included[dependent] = true; + pending.push(dependent); + } + } + } + + subset.clear(); + for (size_t index : topologicalOrder) { + if (index < included.size() && included[index]) { + subset.push_back(index); + } + } + + if (subset.empty()) { + return failResult( + LifecycleErrorCode::NodeResolutionFailed, + nullptr, + "dependent closure is empty", + false + ); + } + + return okResult(); } -LifecycleResult ESPLifecycle::expandSubsetWithDependencies(std::vector& subset) { - if( subset.empty() ){ - return okResult(); - } - - std::vector included(nodes.size(), false); - std::queue pending; - - for( size_t index : subset ){ - if( index >= nodes.size() ){ - return failResult(LifecycleErrorCode::NodeResolutionFailed, nullptr, "subset index out of bounds", false); - } - - if( !included[index] ){ - included[index] = true; - pending.push(index); - } - } - - while( !pending.empty() ){ - const size_t current = pending.front(); - pending.pop(); - - for( size_t dep : nodes[current].dependencyIndexes ){ - if( dep < included.size() && !included[dep] ){ - included[dep] = true; - pending.push(dep); - } - } - } - - subset.clear(); - for( size_t index : topologicalOrder ){ - if( index < included.size() && included[index] ){ - subset.push_back(index); - } - } - - if( subset.empty() ){ - return failResult(LifecycleErrorCode::NodeResolutionFailed, nullptr, "dependency closure is empty", false); - } - - return okResult(); +LifecycleResult ESPLifecycle::expandSubsetWithDependencies(std::vector &subset) { + if (subset.empty()) { + return okResult(); + } + + std::vector included(nodes.size(), false); + std::queue pending; + + for (size_t index : subset) { + if (index >= nodes.size()) { + return failResult( + LifecycleErrorCode::NodeResolutionFailed, + nullptr, + "subset index out of bounds", + false + ); + } + + if (!included[index]) { + included[index] = true; + pending.push(index); + } + } + + while (!pending.empty()) { + const size_t current = pending.front(); + pending.pop(); + + for (size_t dep : nodes[current].dependencyIndexes) { + if (dep < included.size() && !included[dep]) { + included[dep] = true; + pending.push(dep); + } + } + } + + subset.clear(); + for (size_t index : topologicalOrder) { + if (index < included.size() && included[index]) { + subset.push_back(index); + } + } + + if (subset.empty()) { + return failResult( + LifecycleErrorCode::NodeResolutionFailed, + nullptr, + "dependency closure is empty", + false + ); + } + + return okResult(); } -LifecycleResult ESPLifecycle::expandSubsetForReinitialize(std::vector& subset) { - LifecycleResult dependentsResult = expandSubsetWithDependents(subset); - if( !dependentsResult.ok ){ - return dependentsResult; - } +LifecycleResult ESPLifecycle::expandSubsetForReinitialize(std::vector &subset) { + LifecycleResult dependentsResult = expandSubsetWithDependents(subset); + if (!dependentsResult.ok) { + return dependentsResult; + } - LifecycleResult dependenciesResult = expandSubsetWithDependencies(subset); - if( !dependenciesResult.ok ){ - return dependenciesResult; - } + LifecycleResult dependenciesResult = expandSubsetWithDependencies(subset); + if (!dependenciesResult.ok) { + return dependenciesResult; + } - return okResult(); + return okResult(); } diff --git a/src/snapshot.cpp b/src/snapshot.cpp index 5e3f442..ae3abb1 100644 --- a/src/snapshot.cpp +++ b/src/snapshot.cpp @@ -2,158 +2,157 @@ namespace { -const char* stateToText(LifecycleState state) { - switch( state ){ - case LifecycleState::Idle: - return "idle"; - case LifecycleState::Initializing: - return "initializing"; - case LifecycleState::Running: - return "running"; - case LifecycleState::Reinitializing: - return "reinitializing"; - case LifecycleState::Deinitializing: - return "deinitializing"; - case LifecycleState::Failed: - return "failed"; - } - - return "unknown"; +const char *stateToText(LifecycleState state) { + switch (state) { + case LifecycleState::Idle: + return "idle"; + case LifecycleState::Initializing: + return "initializing"; + case LifecycleState::Running: + return "running"; + case LifecycleState::Reinitializing: + return "reinitializing"; + case LifecycleState::Deinitializing: + return "deinitializing"; + case LifecycleState::Failed: + return "failed"; + } + + return "unknown"; } -const char* errorCodeToText(LifecycleErrorCode code) { - switch( code ){ - case LifecycleErrorCode::None: - return "none"; - case LifecycleErrorCode::DuplicateNode: - return "duplicate_node"; - case LifecycleErrorCode::MissingDependency: - return "missing_dependency"; - case LifecycleErrorCode::CycleDetected: - return "cycle_detected"; - case LifecycleErrorCode::Busy: - return "busy"; - case LifecycleErrorCode::InitFailed: - return "init_failed"; - case LifecycleErrorCode::TeardownFailed: - return "teardown_failed"; - case LifecycleErrorCode::InvalidSection: - return "invalid_section"; - case LifecycleErrorCode::InvalidConfig: - return "invalid_config"; - case LifecycleErrorCode::UnknownNode: - return "unknown_node"; - case LifecycleErrorCode::NodeResolutionFailed: - return "node_resolution_failed"; - } - - return "unknown"; +const char *errorCodeToText(LifecycleErrorCode code) { + switch (code) { + case LifecycleErrorCode::None: + return "none"; + case LifecycleErrorCode::DuplicateNode: + return "duplicate_node"; + case LifecycleErrorCode::MissingDependency: + return "missing_dependency"; + case LifecycleErrorCode::CycleDetected: + return "cycle_detected"; + case LifecycleErrorCode::Busy: + return "busy"; + case LifecycleErrorCode::InitFailed: + return "init_failed"; + case LifecycleErrorCode::TeardownFailed: + return "teardown_failed"; + case LifecycleErrorCode::InvalidSection: + return "invalid_section"; + case LifecycleErrorCode::InvalidConfig: + return "invalid_config"; + case LifecycleErrorCode::UnknownNode: + return "unknown_node"; + case LifecycleErrorCode::NodeResolutionFailed: + return "node_resolution_failed"; + } + + return "unknown"; } bool isTransitionState(LifecycleState state) { - return state == LifecycleState::Initializing || - state == LifecycleState::Reinitializing || - state == LifecycleState::Deinitializing; + return state == LifecycleState::Initializing || state == LifecycleState::Reinitializing || + state == LifecycleState::Deinitializing; } -} // namespace +} // namespace -void ESPLifecycle::setState(LifecycleState stateValue, const char* activeNode) { - { - std::lock_guard lock(stateMutex); - currentState = stateValue; - } +void ESPLifecycle::setState(LifecycleState stateValue, const char *activeNode) { + { + std::lock_guard lock(stateMutex); + currentState = stateValue; + } - { - std::lock_guard lock(snapshotMutex); - snapshotValue.state = stateValue; - activeNodeText = activeNode == nullptr ? "" : activeNode; - snapshotValue.activeNode = activeNodeText.empty() ? nullptr : activeNodeText.c_str(); - snapshotValue.updatedAtMs = nowMs(); - } + { + std::lock_guard lock(snapshotMutex); + snapshotValue.state = stateValue; + activeNodeText = activeNode == nullptr ? "" : activeNode; + snapshotValue.activeNode = activeNodeText.empty() ? nullptr : activeNodeText.c_str(); + snapshotValue.updatedAtMs = nowMs(); + } - publishSnapshot(); + publishSnapshot(); } -void ESPLifecycle::setPhase(const char* phaseName) { - std::lock_guard lock(snapshotMutex); - phaseText = phaseName == nullptr ? "idle" : phaseName; +void ESPLifecycle::setPhase(const char *phaseName) { + std::lock_guard lock(snapshotMutex); + phaseText = phaseName == nullptr ? "idle" : phaseName; } LifecycleSnapshot ESPLifecycle::snapshot() const { - std::lock_guard lock(snapshotMutex); - return snapshotValue; + std::lock_guard lock(snapshotMutex); + return snapshotValue; } JsonDocument ESPLifecycle::snapshotJson() const { - JsonDocument document; - - LifecycleSnapshot copy; - std::string activeNode; - std::string phase; - std::string errorNode; - std::string errorDetail; - bool operationOk = true; - - { - std::lock_guard lock(snapshotMutex); - copy = snapshotValue; - activeNode = activeNodeText; - phase = phaseText; - errorNode = nodeNameText; - errorDetail = detailText; - operationOk = lastOperationOk; - } - - document["state"] = stateToText(copy.state); - document["activeNode"] = activeNode.empty() ? nullptr : activeNode.c_str(); - document["completed"] = copy.completed; - document["phaseCompleted"] = !isTransitionState(copy.state); - document["total"] = copy.total; - document["failed"] = copy.failed; - document["errorCode"] = errorCodeToText(copy.errorCode); - document["updatedAtMs"] = copy.updatedAtMs; - - document["phase"] = phase.c_str(); - document["lastOperationOk"] = operationOk; - - JsonObject error = document["lastError"].to(); - error["nodeName"] = errorNode.empty() ? nullptr : errorNode.c_str(); - error["detail"] = errorDetail.empty() ? nullptr : errorDetail.c_str(); - - JsonObject parallel = document["parallel"]["enabled"].to(); - parallel["init"] = config.enableParallelInit; - parallel["deinit"] = config.enableParallelDeinit; - parallel["reinit"] = config.enableParallelReinit; - - return document; + JsonDocument document; + + LifecycleSnapshot copy; + std::string activeNode; + std::string phase; + std::string errorNode; + std::string errorDetail; + bool operationOk = true; + + { + std::lock_guard lock(snapshotMutex); + copy = snapshotValue; + activeNode = activeNodeText; + phase = phaseText; + errorNode = nodeNameText; + errorDetail = detailText; + operationOk = lastOperationOk; + } + + document["state"] = stateToText(copy.state); + document["activeNode"] = activeNode.empty() ? nullptr : activeNode.c_str(); + document["completed"] = copy.completed; + document["phaseCompleted"] = !isTransitionState(copy.state); + document["total"] = copy.total; + document["failed"] = copy.failed; + document["errorCode"] = errorCodeToText(copy.errorCode); + document["updatedAtMs"] = copy.updatedAtMs; + + document["phase"] = phase.c_str(); + document["lastOperationOk"] = operationOk; + + JsonObject error = document["lastError"].to(); + error["nodeName"] = errorNode.empty() ? nullptr : errorNode.c_str(); + error["detail"] = errorDetail.empty() ? nullptr : errorDetail.c_str(); + + JsonObject parallel = document["parallel"]["enabled"].to(); + parallel["init"] = config.enableParallelInit; + parallel["deinit"] = config.enableParallelDeinit; + parallel["reinit"] = config.enableParallelReinit; + + return document; } -void ESPLifecycle::markProgress(const char* activeNode, bool /*completedStep*/) { - { - std::lock_guard lock(snapshotMutex); +void ESPLifecycle::markProgress(const char *activeNode, bool /*completedStep*/) { + { + std::lock_guard lock(snapshotMutex); - activeNodeText = activeNode == nullptr ? "" : activeNode; - snapshotValue.activeNode = activeNodeText.empty() ? nullptr : activeNodeText.c_str(); + activeNodeText = activeNode == nullptr ? "" : activeNode; + snapshotValue.activeNode = activeNodeText.empty() ? nullptr : activeNodeText.c_str(); - uint16_t count = 0; - for( bool isInit : initialized ){ - if( isInit ){ - count++; - } - } + uint16_t count = 0; + for (bool isInit : initialized) { + if (isInit) { + count++; + } + } - snapshotValue.completed = count; - snapshotValue.failed = false; - snapshotValue.errorCode = LifecycleErrorCode::None; - snapshotValue.updatedAtMs = nowMs(); - } + snapshotValue.completed = count; + snapshotValue.failed = false; + snapshotValue.errorCode = LifecycleErrorCode::None; + snapshotValue.updatedAtMs = nowMs(); + } - publishSnapshot(); + publishSnapshot(); } void ESPLifecycle::publishSnapshot() { - if( config.onSnapshot ){ - config.onSnapshot(snapshot()); - } + if (config.onSnapshot) { + config.onSnapshot(snapshot()); + } } diff --git a/src/validation.cpp b/src/validation.cpp index 5ef2bc1..9cbe0e5 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -6,205 +6,255 @@ #include LifecycleResult ESPLifecycle::build() { - std::lock_guard lock(transitionMutex); - return validateAndBuildGraph(); + std::lock_guard lock(transitionMutex); + return validateAndBuildGraph(); } LifecycleResult ESPLifecycle::validateAndBuildGraph() { - if( sections.empty() ){ - ensureSection("default"); - } - - if( nodes.size() > config.maxNodes ){ - return failResult(LifecycleErrorCode::InvalidConfig, nullptr, "maxNodes exceeded", false); - } - - std::unordered_map sectionByName; - sectionByName.reserve(sections.size()); - bool hasDeferredSection = false; - for( size_t i = 0; i < sections.size(); i++ ){ - const std::string& name = sections[i].name; - if( name.empty() ){ - return failResult(LifecycleErrorCode::InvalidSection, nullptr, "empty section name", false); - } - - if( sectionByName.find(name) != sectionByName.end() ){ - return failResult(LifecycleErrorCode::InvalidSection, nullptr, "duplicate section name", false); - } - - sectionByName[name] = i; - - if( sections[i].mode == LifecycleSectionMode::Deferred ){ - hasDeferredSection = true; - if( !sections[i].readinessCheck || !sections[i].waitFn ){ - return failResult( - LifecycleErrorCode::InvalidSection, - nullptr, - "deferred section requires readiness and wait callback", - false - ); - } - } - } - - if( hasDeferredSection && config.worker == nullptr ){ - return failResult( - LifecycleErrorCode::InvalidConfig, - nullptr, - "deferred sections require config.worker", - false - ); - } - - std::unordered_map nodeByName; - nodeByName.reserve(nodes.size()); - - size_t dependencyNameCount = 0; - - for( size_t i = 0; i < nodes.size(); i++ ){ - if( nodes[i].name.empty() ){ - return failResult(LifecycleErrorCode::InvalidConfig, nullptr, "empty node name", false); - } - - if( nodes[i].sectionIndex >= sections.size() ){ - return failResult(LifecycleErrorCode::InvalidSection, nodes[i].name.c_str(), "node references undefined section", false); - } - - if( !nodes[i].initFn || !nodes[i].teardownFn ){ - return failResult(LifecycleErrorCode::InvalidConfig, nodes[i].name.c_str(), "node requires init and teardown callbacks", false); - } - - if( nodeByName.find(nodes[i].name) != nodeByName.end() ){ - return failResult(LifecycleErrorCode::DuplicateNode, nodes[i].name.c_str(), "duplicate node name", false); - } - - nodeByName[nodes[i].name] = i; - - dependencyNameCount += nodes[i].dependenciesByName.size(); - dependencyNameCount += nodes[i].dependentsByName.size(); - } - - if( dependencyNameCount > config.maxDependencies ){ - return failResult(LifecycleErrorCode::InvalidConfig, nullptr, "maxDependencies exceeded", false); - } - - for( auto& node : nodes ){ - node.dependencyIndexes.clear(); - node.reverseDependencyIndexes.clear(); - } - - for( size_t nodeIndex = 0; nodeIndex < nodes.size(); nodeIndex++ ){ - auto& node = nodes[nodeIndex]; - - for( const std::string& depName : node.dependenciesByName ){ - auto it = nodeByName.find(depName); - if( it == nodeByName.end() ){ - return failResult(LifecycleErrorCode::MissingDependency, node.name.c_str(), depName.c_str(), false); - } - node.dependencyIndexes.push_back(it->second); - } - - for( const std::string& dependentName : node.dependentsByName ){ - auto it = nodeByName.find(dependentName); - if( it == nodeByName.end() ){ - return failResult(LifecycleErrorCode::MissingDependency, node.name.c_str(), dependentName.c_str(), false); - } - nodes[it->second].dependencyIndexes.push_back(nodeIndex); - } - } - - for( size_t nodeIndex = 0; nodeIndex < nodes.size(); nodeIndex++ ){ - auto& deps = nodes[nodeIndex].dependencyIndexes; - std::sort(deps.begin(), deps.end()); - deps.erase(std::unique(deps.begin(), deps.end()), deps.end()); - - for( size_t depIndex : deps ){ - if( depIndex >= nodes.size() ){ - return failResult(LifecycleErrorCode::InvalidConfig, nodes[nodeIndex].name.c_str(), "dependency index invalid", false); - } - - if( nodes[depIndex].sectionIndex > nodes[nodeIndex].sectionIndex ){ - return failResult( - LifecycleErrorCode::InvalidConfig, - nodes[nodeIndex].name.c_str(), - "dependency points to future section", - false - ); - } - - nodes[depIndex].reverseDependencyIndexes.push_back(nodeIndex); - } - } - - for( auto& node : nodes ){ - auto& rev = node.reverseDependencyIndexes; - std::sort(rev.begin(), rev.end()); - rev.erase(std::unique(rev.begin(), rev.end()), rev.end()); - } - - LifecycleResult cycleResult = ensureNoCycles(); - if( !cycleResult.ok ){ - return cycleResult; - } - - initialized.assign(nodes.size(), false); - graphBuilt = true; - - { - std::lock_guard lock(snapshotMutex); - snapshotValue.total = static_cast(nodes.size()); - snapshotValue.completed = 0; - snapshotValue.failed = false; - snapshotValue.errorCode = LifecycleErrorCode::None; - snapshotValue.updatedAtMs = nowMs(); - } - publishSnapshot(); - - return okResult("graph built"); + if (sections.empty()) { + ensureSection("default"); + } + + if (nodes.size() > config.maxNodes) { + return failResult(LifecycleErrorCode::InvalidConfig, nullptr, "maxNodes exceeded", false); + } + + std::unordered_map sectionByName; + sectionByName.reserve(sections.size()); + bool hasDeferredSection = false; + for (size_t i = 0; i < sections.size(); i++) { + const std::string &name = sections[i].name; + if (name.empty()) { + return failResult( + LifecycleErrorCode::InvalidSection, + nullptr, + "empty section name", + false + ); + } + + if (sectionByName.find(name) != sectionByName.end()) { + return failResult( + LifecycleErrorCode::InvalidSection, + nullptr, + "duplicate section name", + false + ); + } + + sectionByName[name] = i; + + if (sections[i].mode == LifecycleSectionMode::Deferred) { + hasDeferredSection = true; + if (!sections[i].readinessCheck || !sections[i].waitFn) { + return failResult( + LifecycleErrorCode::InvalidSection, + nullptr, + "deferred section requires readiness and wait callback", + false + ); + } + } + } + + if (hasDeferredSection && config.worker == nullptr) { + return failResult( + LifecycleErrorCode::InvalidConfig, + nullptr, + "deferred sections require config.worker", + false + ); + } + + std::unordered_map nodeByName; + nodeByName.reserve(nodes.size()); + + size_t dependencyNameCount = 0; + + for (size_t i = 0; i < nodes.size(); i++) { + if (nodes[i].name.empty()) { + return failResult(LifecycleErrorCode::InvalidConfig, nullptr, "empty node name", false); + } + + if (nodes[i].sectionIndex >= sections.size()) { + return failResult( + LifecycleErrorCode::InvalidSection, + nodes[i].name.c_str(), + "node references undefined section", + false + ); + } + + if (!nodes[i].initFn || !nodes[i].teardownFn) { + return failResult( + LifecycleErrorCode::InvalidConfig, + nodes[i].name.c_str(), + "node requires init and teardown callbacks", + false + ); + } + + if (nodeByName.find(nodes[i].name) != nodeByName.end()) { + return failResult( + LifecycleErrorCode::DuplicateNode, + nodes[i].name.c_str(), + "duplicate node name", + false + ); + } + + nodeByName[nodes[i].name] = i; + + dependencyNameCount += nodes[i].dependenciesByName.size(); + dependencyNameCount += nodes[i].dependentsByName.size(); + } + + if (dependencyNameCount > config.maxDependencies) { + return failResult( + LifecycleErrorCode::InvalidConfig, + nullptr, + "maxDependencies exceeded", + false + ); + } + + for (auto &node : nodes) { + node.dependencyIndexes.clear(); + node.reverseDependencyIndexes.clear(); + } + + for (size_t nodeIndex = 0; nodeIndex < nodes.size(); nodeIndex++) { + auto &node = nodes[nodeIndex]; + + for (const std::string &depName : node.dependenciesByName) { + auto it = nodeByName.find(depName); + if (it == nodeByName.end()) { + return failResult( + LifecycleErrorCode::MissingDependency, + node.name.c_str(), + depName.c_str(), + false + ); + } + node.dependencyIndexes.push_back(it->second); + } + + for (const std::string &dependentName : node.dependentsByName) { + auto it = nodeByName.find(dependentName); + if (it == nodeByName.end()) { + return failResult( + LifecycleErrorCode::MissingDependency, + node.name.c_str(), + dependentName.c_str(), + false + ); + } + nodes[it->second].dependencyIndexes.push_back(nodeIndex); + } + } + + for (size_t nodeIndex = 0; nodeIndex < nodes.size(); nodeIndex++) { + auto &deps = nodes[nodeIndex].dependencyIndexes; + std::sort(deps.begin(), deps.end()); + deps.erase(std::unique(deps.begin(), deps.end()), deps.end()); + + for (size_t depIndex : deps) { + if (depIndex >= nodes.size()) { + return failResult( + LifecycleErrorCode::InvalidConfig, + nodes[nodeIndex].name.c_str(), + "dependency index invalid", + false + ); + } + + if (nodes[depIndex].sectionIndex > nodes[nodeIndex].sectionIndex) { + return failResult( + LifecycleErrorCode::InvalidConfig, + nodes[nodeIndex].name.c_str(), + "dependency points to future section", + false + ); + } + + nodes[depIndex].reverseDependencyIndexes.push_back(nodeIndex); + } + } + + for (auto &node : nodes) { + auto &rev = node.reverseDependencyIndexes; + std::sort(rev.begin(), rev.end()); + rev.erase(std::unique(rev.begin(), rev.end()), rev.end()); + } + + LifecycleResult cycleResult = ensureNoCycles(); + if (!cycleResult.ok) { + return cycleResult; + } + + initialized.assign(nodes.size(), false); + graphBuilt = true; + + { + std::lock_guard lock(snapshotMutex); + snapshotValue.total = static_cast(nodes.size()); + snapshotValue.completed = 0; + snapshotValue.failed = false; + snapshotValue.errorCode = LifecycleErrorCode::None; + snapshotValue.updatedAtMs = nowMs(); + } + publishSnapshot(); + + return okResult("graph built"); } LifecycleResult ESPLifecycle::ensureNoCycles() { - std::vector indegree(nodes.size(), 0); - std::queue ready; - - for( size_t i = 0; i < nodes.size(); i++ ){ - indegree[i] = nodes[i].dependencyIndexes.size(); - if( indegree[i] == 0 ){ - ready.push(i); - } - } - - topologicalOrder.clear(); - topologicalOrder.reserve(nodes.size()); - - while( !ready.empty() ){ - const size_t current = ready.front(); - ready.pop(); - topologicalOrder.push_back(current); - - for( size_t dependent : nodes[current].reverseDependencyIndexes ){ - if( dependent >= indegree.size() ){ - continue; - } - - if( indegree[dependent] == 0 ){ - continue; - } - - indegree[dependent]--; - if( indegree[dependent] == 0 ){ - ready.push(dependent); - } - } - } - - if( topologicalOrder.size() != nodes.size() ){ - return failResult(LifecycleErrorCode::CycleDetected, nullptr, "dependency cycle detected", false); - } - - return okResult(); + std::vector indegree(nodes.size(), 0); + std::queue ready; + + for (size_t i = 0; i < nodes.size(); i++) { + indegree[i] = nodes[i].dependencyIndexes.size(); + if (indegree[i] == 0) { + ready.push(i); + } + } + + topologicalOrder.clear(); + topologicalOrder.reserve(nodes.size()); + + while (!ready.empty()) { + const size_t current = ready.front(); + ready.pop(); + topologicalOrder.push_back(current); + + for (size_t dependent : nodes[current].reverseDependencyIndexes) { + if (dependent >= indegree.size()) { + continue; + } + + if (indegree[dependent] == 0) { + continue; + } + + indegree[dependent]--; + if (indegree[dependent] == 0) { + ready.push(dependent); + } + } + } + + if (topologicalOrder.size() != nodes.size()) { + return failResult( + LifecycleErrorCode::CycleDetected, + nullptr, + "dependency cycle detected", + false + ); + } + + return okResult(); } LifecycleResult ESPLifecycle::resolveIndexes() { - return validateAndBuildGraph(); + return validateAndBuildGraph(); }