diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6e1a1d8..24901f8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,19 +24,45 @@ jobs: uses: actions/checkout@v4 - name: Audit production sources + shell: bash run: | if grep -RInE '(^|[^[:alnum:]_])throw([^[:alnum:]_]|$)|std::abort[[:space:]]*\(' src; then echo "Embedded safety audit failed" exit 1 fi - if grep -RInE '#include[[:space:]]+[<"](Fresh|Pulse|Signal|Tempo|Trace|Worker|Vault|Link|Courier|Flow|Lingo)\\.h[>"]' src; then + if grep -RInE '#include[[:space:]]+[<"](Fresh|Pulse|Signal|Tempo|Trace|Worker|Vault|Link|Courier|Flow|Lingo)\.h[>"]' src; then echo "Embedded safety audit failed: Phase must not depend on other ZekStack libraries" exit 1 fi + if [[ "$GITHUB_REF" == refs/tags/v* ]]; then + python3 scripts/check_release_version.py --tag "$GITHUB_REF_NAME" + else + python3 scripts/check_release_version.py + fi - build-examples: + host-tests: runs-on: ubuntu-latest needs: source-audit + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build host tests + run: | + g++ -std=c++20 -pthread -Wall -Wextra -Werror \ + -Itests/host/stubs -Isrc \ + src/Phase.cpp \ + tests/host/semaphore_stubs.cpp \ + tests/host/task_stubs.cpp \ + tests/host/test_phase.cpp \ + -o phase-host-tests + + - name: Run host tests + run: ./phase-host-tests + + build-examples: + runs-on: ubuntu-latest + needs: [source-audit, host-tests] strategy: fail-fast: false matrix: @@ -81,45 +107,9 @@ jobs: fi done - release: - if: startsWith(github.ref, 'refs/tags/v') - needs: [source-audit, build-examples, arduino-cli] - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: '3.12' - - - name: Generate release changelog - run: | - python3 scripts/generate_release_changelog.py \ - --target-ref "${GITHUB_REF_NAME}" \ - --tag-name "${GITHUB_REF_NAME}" \ - --output "release-changelog.md" - - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ github.ref_name }} - target_commitish: ${{ github.sha }} - draft: false - prerelease: ${{ contains(github.ref_name, '-') }} - generate_release_notes: false - body_path: release-changelog.md - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - arduino-cli: runs-on: ubuntu-latest - needs: source-audit + needs: [source-audit, host-tests] strategy: fail-fast: false matrix: @@ -177,3 +167,39 @@ jobs: "$d" fi done + + release: + if: startsWith(github.ref, 'refs/tags/v') + needs: [source-audit, host-tests, build-examples, arduino-cli] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Generate release changelog + run: | + python3 scripts/generate_release_changelog.py \ + --target-ref "${GITHUB_REF_NAME}" \ + --tag-name "${GITHUB_REF_NAME}" \ + --output "release-changelog.md" + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref_name }} + target_commitish: ${{ github.sha }} + draft: false + prerelease: ${{ contains(github.ref_name, '-') }} + generate_release_notes: false + body_path: release-changelog.md + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index d57c304..8cf34e9 100644 --- a/README.md +++ b/README.md @@ -93,9 +93,12 @@ void loop() { * `pause()` is async and takes effect before the next lifecycle action or group-condition poll. * Callback timeout checks happen after the callback returns. * Group condition polling timeouts are enforced by the Phase task. -* Registration closes after `start()`. -* The destructor waits for the Phase task to end. If user lifecycle code never returns, destruction can block forever. -* Phase uses bounded node and dependency counts, but registration currently uses dynamic allocation through `std::vector`, `std::string`, and `std::function`. Register nodes during setup before runtime work starts. +* Registration closes after a successful `start()` request. +* `stop()`, `pause()`, and `resume()` may be called from Phase callbacks. `end()` must be called from another task and returns `Busy` when called from the Phase task. +* The destructor waits for the Phase task to stop using its internal state. Destruction from a Phase callback is deferred safely until the worker exits. +* Registration and graph preparation use `std::vector`, `std::string`, and `std::function`. Node storage, dependency indexes, and lifecycle order are preallocated before the worker starts; lifecycle execution does not allocate. +* `PhaseChange` string pointers are valid for the complete callback invocation. Event messages and pause reasons are copied into bounded internal snapshots and may be truncated to 191 characters. +* Stop/deinit failures are best-effort and are reported through `onChange()` while remaining cleanup continues. * Phase does not depend on other ZekStack libraries. ## Examples @@ -154,7 +157,7 @@ For the full API, see [`docs/api.md`](docs/api.md). | PSRAM | Optional for task stacks when ESP-IDF support is available | | Dependencies | none | | Exceptions | Not used | -| Status | Early-stage `0.0.1` | +| Status | `0.1.0` release candidate | ## Configuration diff --git a/library.json b/library.json index c5d7029..acfe318 100644 --- a/library.json +++ b/library.json @@ -1,6 +1,6 @@ { "name": "Phase", - "version": "0.0.1", + "version": "0.1.0", "description": "Async application lifecycle orchestration library for ESP32.", "keywords": [ "esp32", diff --git a/library.properties b/library.properties index 7958501..ccce000 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=Phase -version=0.0.1 +version=0.1.0 author=zekageri maintainer=zekageri sentence=Application lifecycle orchestration library for ESP32. diff --git a/scripts/check_release_version.py b/scripts/check_release_version.py new file mode 100644 index 0000000..32bed24 --- /dev/null +++ b/scripts/check_release_version.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 + +import argparse +import json +from pathlib import Path + + +def properties_version(path: Path) -> str: + for line in path.read_text(encoding="utf-8").splitlines(): + if line.startswith("version="): + return line.split("=", 1)[1].strip() + raise SystemExit(f"version is missing from {path}") + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--tag", default="") + args = parser.parse_args() + + json_version = json.loads(Path("library.json").read_text(encoding="utf-8"))["version"] + properties = properties_version(Path("library.properties")) + if json_version != properties: + raise SystemExit( + f"manifest version mismatch: library.json={json_version}, library.properties={properties}" + ) + + if args.tag: + expected = args.tag[1:] if args.tag.startswith("v") else args.tag + if json_version != expected: + raise SystemExit(f"tag {args.tag} does not match manifest version {json_version}") + + print(f"Phase release metadata is consistent: {json_version}") + + +if __name__ == "__main__": + main() diff --git a/src/Phase.cpp b/src/Phase.cpp index f49bb4c..6166614 100644 --- a/src/Phase.cpp +++ b/src/Phase.cpp @@ -1,1871 +1,8 @@ -#include "Phase.h" - -#include "internal/PhaseMutex.h" -#include "internal/PhaseTaskSupport.h" - -#include -#include -#include -#include -#include - -constexpr uint32_t kWaitPollMs = 10; -constexpr uint32_t kTaskStartTimeoutMs = 1000; -constexpr uint8_t kInitTimeout = 0; -constexpr uint8_t kStartTimeout = 1; -constexpr uint8_t kStopTimeout = 2; -constexpr uint8_t kDeinitTimeout = 3; - -enum class DependencyState : uint8_t { - Ready, - Waiting, - FailedRequired, - SkipOptional, -}; - -struct PhaseNode { - PhaseNodeType type = PhaseNodeType::Step; - std::string name; - std::vector dependencies; - PhaseCallback initCallback; - PhaseCallback deinitCallback; - PhaseCallback startCallback; - PhaseCallback stopCallback; - PhaseConditionCallback conditionCallback; - bool optional = false; - bool initialized = false; - bool started = false; - bool ready = false; - bool failed = false; - bool skipped = false; - bool hasInitTimeout = false; - bool hasStartTimeout = false; - bool hasStopTimeout = false; - bool hasDeinitTimeout = false; - bool hasGroupTimeout = false; - bool hasPollInterval = false; - uint32_t initTimeoutMs = 0; - uint32_t startTimeoutMs = 0; - uint32_t stopTimeoutMs = 0; - uint32_t deinitTimeoutMs = 0; - uint32_t groupTimeoutMs = 0; - uint32_t pollIntervalMs = 0; -}; - -struct PhaseNodeRuntimeSnapshot { - size_t index = 0; - PhaseNodeType type = PhaseNodeType::None; - std::string name; - std::vector dependencies; - bool optional = false; - bool initialized = false; - bool started = false; - bool ready = false; - bool failed = false; - bool skipped = false; - bool hasStartCallback = false; -}; - -struct PhaseStepCallbackSnapshot { - std::string name; - PhaseNodeType type = PhaseNodeType::Step; - bool optional = false; - PhaseCallback initCallback; - PhaseCallback deinitCallback; - PhaseCallback startCallback; - PhaseCallback stopCallback; - uint32_t initTimeoutMs = 0; - uint32_t startTimeoutMs = 0; - uint32_t stopTimeoutMs = 0; - uint32_t deinitTimeoutMs = 0; -}; - -struct PhaseGroupCallbackSnapshot { - std::string name; - PhaseNodeType type = PhaseNodeType::Group; - bool optional = false; - PhaseConditionCallback conditionCallback; - uint32_t timeoutMs = 0; - uint32_t pollMs = 0; -}; - -struct PhaseImpl { - PhaseConfig config{}; - PhaseMutex mutex; - std::vector nodes; - std::vector initOrder; - std::vector startOrder; - PhaseChangeCallback changeCallback; - PhaseReadyCallback readyCallback; - PhaseFailedCallback failedCallback; - TaskHandle_t taskHandle = nullptr; - SemaphoreHandle_t taskStarted = nullptr; - bool createdWithCaps = false; - bool initialized = false; - bool registrationClosed = false; - bool startRequested = false; - bool stopRequested = false; - bool ending = false; - bool taskRunning = false; - bool paused = false; - std::string pauseReason; - PhaseState currentState = PhaseState::Idle; - PhaseStackType actualStackType = PhaseStackType::Internal; - uint32_t bootCount = 0; - uint32_t rollbackCount = 0; - uint32_t changeCount = 0; - size_t stackHighWaterMarkBytes = 0; - - ~PhaseImpl() { - if (taskStarted != nullptr) { - vSemaphoreDelete(taskStarted); - } - } - - static void taskEntry(void *arg) { - static_cast(arg)->taskLoop(); - } - - PhaseResult notifyTask() { - if (taskHandle == nullptr) { - return PhaseResult::failure(PhaseStatus::NotInitialized, "phase task is not available"); - } - xTaskNotifyGive(taskHandle); - return PhaseResult::success(); - } - - void setState(PhaseState state) { - PhaseLock lock(mutex); - if (!lock) { - return; - } - currentState = state; - } - - bool isEnding() { - PhaseLock lock(mutex); - return lock && ending; - } - - bool shouldStop() { - PhaseLock lock(mutex); - return lock && stopRequested; - } - - bool isPausedFlag() { - PhaseLock lock(mutex); - return lock && paused; - } - - const char *pauseReasonText() { - return pauseReason.empty() ? nullptr : pauseReason.c_str(); - } - - void emitChange( - PhaseState state, - PhaseNodeType nodeType, - const char *nodeName, - const char *message, - PhaseResult result = PhaseResult::success(), - uint32_t durationMs = 0 - ) { - PhaseChangeCallback callback; - PhaseChange change; - { - PhaseLock lock(mutex); - if (!lock) { - return; - } - currentState = state; - changeCount++; - callback = changeCallback; - change.state = state; - change.nodeType = nodeType; - change.nodeName = nodeName; - change.pauseReason = pauseReasonText(); - change.message = message != nullptr ? message : result.message; - change.isBooting = state == PhaseState::Booting; - change.isStarting = state == PhaseState::Starting; - change.isPaused = paused || state == PhaseState::Paused; - change.isStopping = state == PhaseState::Stopping; - change.isDeinitializing = state == PhaseState::Deinitializing; - change.isDone = - state == PhaseState::Ready || state == PhaseState::Stopped || - state == PhaseState::Failed || state == PhaseState::Ended; - change.hasError = !result; - change.result = result; - change.durationMs = durationMs; - } - if (callback) { - callback(change); - } - } - - void emitReady() { - PhaseReadyCallback callback; - { - PhaseLock lock(mutex); - if (!lock) { - return; - } - callback = readyCallback; - } - if (callback) { - callback(); - } - } - - void emitFailed(PhaseResult result) { - PhaseFailedCallback callback; - { - PhaseLock lock(mutex); - if (!lock) { - return; - } - callback = failedCallback; - } - if (callback) { - callback(result); - } - } - - size_t findNodeIndex(const std::string &name) const { - for (size_t i = 0; i < nodes.size(); ++i) { - if (nodes[i].name == name) { - return i; - } - } - return nodes.size(); - } - - PhaseNode *findNode(const std::string &name) { - const size_t index = findNodeIndex(name); - return index < nodes.size() ? &nodes[index] : nullptr; - } - - PhaseResult validateRegistrationOpen() { - if (!initialized) { - return PhaseResult::failure(PhaseStatus::NotInitialized, "phase is not initialized"); - } - if (registrationClosed) { - return PhaseResult::failure( - PhaseStatus::RegistrationClosed, - "registration is closed after start" - ); - } - return PhaseResult::success(); - } - - PhaseResult validateGraph() { - if (nodes.empty()) { - return PhaseResult::failure(PhaseStatus::InvalidArgument, "at least one node is required"); - } - for (size_t i = 0; i < nodes.size(); ++i) { - PhaseNode &node = nodes[i]; - if (node.name.empty()) { - return PhaseResult::failure(PhaseStatus::InvalidArgument, "node name is required"); - } - if (node.type == PhaseNodeType::Step && !node.initCallback) { - return PhaseResult::failure(PhaseStatus::InvalidCallback, "init callback is required"); - } - if (node.dependencies.size() > config.maxDependenciesPerNode) { - return PhaseResult::failure( - PhaseStatus::TooManyDependencies, - "too many dependencies" - ); - } - for (size_t j = i + 1; j < nodes.size(); ++j) { - if (node.name == nodes[j].name) { - return PhaseResult::failure(PhaseStatus::DuplicateName, "duplicate node name"); - } - } - for (const std::string &dependency : node.dependencies) { - if (dependency.empty()) { - return PhaseResult::failure( - PhaseStatus::InvalidArgument, - "dependency name is required" - ); - } - if (findNodeIndex(dependency) >= nodes.size()) { - return PhaseResult::failure( - PhaseStatus::MissingDependency, - "dependency was not registered" - ); - } - } - } - - std::vector marks(nodes.size(), 0); - for (size_t i = 0; i < nodes.size(); ++i) { - if (hasCycle(i, marks)) { - return PhaseResult::failure( - PhaseStatus::CircularDependency, - "dependency cycle detected" - ); - } - } - return PhaseResult::success(); - } - - bool hasCycle(size_t index, std::vector &marks) { - if (marks[index] == 1) { - return true; - } - if (marks[index] == 2) { - return false; - } - marks[index] = 1; - for (const std::string &dependency : nodes[index].dependencies) { - const size_t dependencyIndex = findNodeIndex(dependency); - if (dependencyIndex < nodes.size() && hasCycle(dependencyIndex, marks)) { - return true; - } - } - marks[index] = 2; - return false; - } - - size_t nodeCount() { - PhaseLock lock(mutex); - if (!lock) { - return 0; - } - return nodes.size(); - } - - bool getNodeRuntimeSnapshot(size_t index, PhaseNodeRuntimeSnapshot &out) { - PhaseLock lock(mutex); - if (!lock || index >= nodes.size()) { - return false; - } - const PhaseNode &node = nodes[index]; - out.index = index; - out.type = node.type; - out.name = node.name; - out.dependencies = node.dependencies; - out.optional = node.optional; - out.initialized = node.initialized; - out.started = node.started; - out.ready = node.ready; - out.failed = node.failed; - out.skipped = node.skipped; - out.hasStartCallback = static_cast(node.startCallback); - return true; - } - - bool getNodeRuntimeSnapshot(const std::string &name, PhaseNodeRuntimeSnapshot &out) { - PhaseLock lock(mutex); - if (!lock) { - return false; - } - for (size_t i = 0; i < nodes.size(); ++i) { - const PhaseNode &node = nodes[i]; - if (node.name != name) { - continue; - } - out.index = i; - out.type = node.type; - out.name = node.name; - out.dependencies = node.dependencies; - out.optional = node.optional; - out.initialized = node.initialized; - out.started = node.started; - out.ready = node.ready; - out.failed = node.failed; - out.skipped = node.skipped; - out.hasStartCallback = static_cast(node.startCallback); - return true; - } - return false; - } - - bool getStepCallbackSnapshot(size_t index, PhaseStepCallbackSnapshot &out) { - PhaseLock lock(mutex); - if (!lock || index >= nodes.size() || nodes[index].type != PhaseNodeType::Step) { - return false; - } - const PhaseNode &node = nodes[index]; - out.name = node.name; - out.type = node.type; - out.optional = node.optional; - out.initCallback = node.initCallback; - out.deinitCallback = node.deinitCallback; - out.startCallback = node.startCallback; - out.stopCallback = node.stopCallback; - out.initTimeoutMs = resolveTimeout(node, kInitTimeout); - out.startTimeoutMs = resolveTimeout(node, kStartTimeout); - out.stopTimeoutMs = resolveTimeout(node, kStopTimeout); - out.deinitTimeoutMs = resolveTimeout(node, kDeinitTimeout); - return true; - } - - bool getGroupCallbackSnapshot(size_t index, PhaseGroupCallbackSnapshot &out) { - PhaseLock lock(mutex); - if (!lock || index >= nodes.size() || nodes[index].type != PhaseNodeType::Group) { - return false; - } - const PhaseNode &node = nodes[index]; - out.name = node.name; - out.type = node.type; - out.optional = node.optional; - out.conditionCallback = node.conditionCallback; - out.timeoutMs = node.hasGroupTimeout ? node.groupTimeoutMs : config.defaultGroupTimeoutMs; - out.pollMs = node.hasPollInterval ? node.pollIntervalMs : config.conditionPollIntervalMs; - return true; - } - - void resetRunState() { - PhaseLock lock(mutex); - if (!lock) { - return; - } - initOrder.clear(); - startOrder.clear(); - for (PhaseNode &node : nodes) { - node.initialized = false; - node.started = false; - node.ready = false; - node.failed = false; - node.skipped = false; - } - } - - DependencyState initDependencyState(const PhaseNodeRuntimeSnapshot &node) { - for (const std::string &dependencyName : node.dependencies) { - PhaseNodeRuntimeSnapshot dependency; - if (!getNodeRuntimeSnapshot(dependencyName, dependency)) { - return DependencyState::FailedRequired; - } - if (dependency.type == PhaseNodeType::Group) { - continue; - } - if (dependency.failed || dependency.skipped) { - return node.optional ? DependencyState::SkipOptional : DependencyState::FailedRequired; - } - if (!dependency.initialized) { - return DependencyState::Waiting; - } - } - return DependencyState::Ready; - } - - DependencyState readinessDependencyState(const PhaseNodeRuntimeSnapshot &node) { - for (const std::string &dependencyName : node.dependencies) { - PhaseNodeRuntimeSnapshot dependency; - if (!getNodeRuntimeSnapshot(dependencyName, dependency)) { - return DependencyState::FailedRequired; - } - if (dependency.failed || dependency.skipped) { - return node.optional ? DependencyState::SkipOptional : DependencyState::FailedRequired; - } - if (!dependency.ready) { - return DependencyState::Waiting; - } - } - return DependencyState::Ready; - } - - bool allStepsInitialized() { - PhaseLock lock(mutex); - if (!lock) { - return false; - } - for (const PhaseNode &node : nodes) { - if (node.type == PhaseNodeType::Step && !node.initialized && !node.failed && !node.skipped) { - return false; - } - } - return true; - } - - bool allNodesDone() { - PhaseLock lock(mutex); - if (!lock) { - return false; - } - for (const PhaseNode &node : nodes) { - if (!node.ready && !node.failed && !node.skipped) { - return false; - } - } - return true; - } - - bool waitIfPaused(bool ignoreStop = false) { - if (ignoreStop) { - return true; - } - bool emitted = false; - PhaseState resumeState = PhaseState::Booting; - { - PhaseLock lock(mutex); - if (lock) { - resumeState = currentState; - } - } - while (isPausedFlag() && !shouldStop() && !isEnding()) { - if (!emitted) { - emitChange(PhaseState::Paused, PhaseNodeType::None, nullptr, "phase paused"); - emitted = true; - } - ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(kWaitPollMs)); - } - if (emitted && !shouldStop() && !isEnding()) { - emitChange(resumeState, PhaseNodeType::None, nullptr, "phase resumed"); - } - return !shouldStop() && !isEnding(); - } - - uint32_t resolveTimeout(const PhaseNode &node, uint8_t kind) const { - switch (kind) { - case kInitTimeout: - return node.hasInitTimeout ? node.initTimeoutMs : config.defaultInitTimeoutMs; - case kStartTimeout: - return node.hasStartTimeout ? node.startTimeoutMs : config.defaultStartTimeoutMs; - case kStopTimeout: - return node.hasStopTimeout ? node.stopTimeoutMs : config.defaultStopTimeoutMs; - case kDeinitTimeout: - return node.hasDeinitTimeout ? node.deinitTimeoutMs : config.defaultDeinitTimeoutMs; - default: - return 0; - } - } - - PhaseResult runLifecycleCallback( - const std::string &name, - PhaseNodeType type, - uint32_t timeoutMs, - PhaseCallback callback, - PhaseState state, - const char *successMessage, - bool ignoreStop = false - ) { - if (!callback) { - return PhaseResult::success(); - } - if (!waitIfPaused(ignoreStop)) { - return PhaseResult::failure(PhaseStatus::Busy, "phase stopped"); - } - const uint32_t startMs = millis(); - emitChange(state, type, name.c_str(), successMessage); - PhaseResult result = callback(); - const uint32_t elapsedMs = millis() - startMs; - if (result && timeoutMs > 0 && elapsedMs > timeoutMs) { - result = PhaseResult::failure(PhaseStatus::Timeout, "callback timed out"); - } - emitChange(state, type, name.c_str(), result.message, result, elapsedMs); - return result; - } - - PhaseResult waitForGroup(size_t index) { - PhaseGroupCallbackSnapshot group; - if (!getGroupCallbackSnapshot(index, group)) { - return PhaseResult::failure(PhaseStatus::InvalidArgument, "invalid group"); - } - if (!waitIfPaused()) { - return PhaseResult::failure(PhaseStatus::Busy, "phase stopped"); - } - uint32_t elapsedMs = 0; - emitChange(PhaseState::Starting, group.type, group.name.c_str(), "waiting for group"); - while (!shouldStop() && !isEnding()) { - if (!waitIfPaused()) { - return PhaseResult::failure(PhaseStatus::Busy, "phase stopped"); - } - if (!group.conditionCallback || group.conditionCallback()) { - emitChange(PhaseState::Starting, group.type, group.name.c_str(), "group ready"); - return PhaseResult::success("group ready"); - } - if (group.timeoutMs > 0 && elapsedMs >= group.timeoutMs) { - const PhaseResult result = - PhaseResult::failure(PhaseStatus::Timeout, "group condition timed out"); - emitChange(PhaseState::Starting, group.type, group.name.c_str(), result.message, result); - return result; - } - const uint32_t delayMs = group.pollMs == 0 ? 1 : group.pollMs; - ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(delayMs)); - if (!isPausedFlag()) { - elapsedMs += delayMs; - } - } - return PhaseResult::failure(PhaseStatus::Busy, "phase stopped"); - } - - PhaseResult markOptionalFailure( - size_t index, - PhaseResult result, - PhaseState state = PhaseState::Booting - ) { - std::string name; - PhaseNodeType type = PhaseNodeType::None; - { - PhaseLock lock(mutex); - if (!lock || index >= nodes.size()) { - return PhaseResult::failure(PhaseStatus::InvalidArgument, "invalid node"); - } - PhaseNode &node = nodes[index]; - node.failed = true; - node.ready = false; - name = node.name; - type = node.type; - } - emitChange(state, type, name.c_str(), result.message, result); - return PhaseResult::success("optional node failed"); - } - - PhaseResult runOneInitAction(bool &madeProgress) { - madeProgress = false; - const size_t count = nodeCount(); - for (size_t i = 0; i < count; ++i) { - PhaseNodeRuntimeSnapshot node; - if (!getNodeRuntimeSnapshot(i, node)) { - continue; - } - if (node.type != PhaseNodeType::Step || node.initialized || node.failed || node.skipped) { - continue; - } - const DependencyState deps = initDependencyState(node); - if (deps == DependencyState::Waiting) { - continue; - } - if (deps == DependencyState::SkipOptional) { - { - PhaseLock lock(mutex); - if (lock) { - nodes[i].skipped = true; - } - } - madeProgress = true; - emitChange( - PhaseState::Booting, - node.type, - node.name.c_str(), - "optional node skipped" - ); - return PhaseResult::success(); - } - if (deps == DependencyState::FailedRequired) { - { - PhaseLock lock(mutex); - if (lock) { - nodes[i].failed = true; - } - } - madeProgress = true; - return PhaseResult::failure( - PhaseStatus::DependencyFailed, - "required dependency failed" - ); - } - - PhaseStepCallbackSnapshot step; - if (!getStepCallbackSnapshot(i, step)) { - continue; - } - PhaseResult result = runLifecycleCallback( - step.name, - step.type, - step.initTimeoutMs, - step.initCallback, - PhaseState::Booting, - "initializing step" - ); - madeProgress = true; - if (!result) { - if (step.optional) { - return markOptionalFailure(i, result); - } - PhaseLock lock(mutex); - if (lock) { - nodes[i].failed = true; - } - return result; - } - { - PhaseLock lock(mutex); - if (lock && i < nodes.size()) { - nodes[i].initialized = true; - initOrder.push_back(i); - } - } - return result; - } - return PhaseResult::success(); - } - - PhaseResult runOneReadinessAction(bool &madeProgress) { - madeProgress = false; - const size_t count = nodeCount(); - for (size_t i = 0; i < count; ++i) { - PhaseNodeRuntimeSnapshot node; - if (!getNodeRuntimeSnapshot(i, node)) { - continue; - } - if (node.ready || node.failed || node.skipped) { - continue; - } - const DependencyState deps = readinessDependencyState(node); - if (deps == DependencyState::Waiting) { - continue; - } - if (deps == DependencyState::SkipOptional) { - { - PhaseLock lock(mutex); - if (lock) { - nodes[i].skipped = true; - } - } - madeProgress = true; - emitChange( - PhaseState::Starting, - node.type, - node.name.c_str(), - "optional node skipped" - ); - return PhaseResult::success(); - } - if (deps == DependencyState::FailedRequired) { - PhaseLock lock(mutex); - if (lock) { - nodes[i].failed = true; - } - madeProgress = true; - return PhaseResult::failure( - PhaseStatus::DependencyFailed, - "required dependency failed" - ); - } - - if (node.type == PhaseNodeType::Group) { - PhaseResult result = waitForGroup(i); - madeProgress = true; - if (result) { - PhaseLock lock(mutex); - if (lock) { - nodes[i].ready = true; - } - return result; - } - if (node.optional) { - return markOptionalFailure(i, result, PhaseState::Starting); - } - PhaseLock lock(mutex); - if (lock) { - nodes[i].failed = true; - } - return result; - } - - if (!node.initialized) { - continue; - } - if (!node.hasStartCallback) { - PhaseLock lock(mutex); - if (lock) { - nodes[i].ready = true; - } - madeProgress = true; - emitChange(PhaseState::Starting, node.type, node.name.c_str(), "step ready"); - return PhaseResult::success("step ready"); - } - - PhaseStepCallbackSnapshot step; - if (!getStepCallbackSnapshot(i, step)) { - continue; - } - PhaseResult result = runLifecycleCallback( - step.name, - step.type, - step.startTimeoutMs, - step.startCallback, - PhaseState::Starting, - "starting step" - ); - madeProgress = true; - if (!result) { - if (step.optional) { - if (step.deinitCallback) { - (void)runLifecycleCallback( - step.name, - step.type, - step.deinitTimeoutMs, - step.deinitCallback, - PhaseState::Deinitializing, - "deinitializing optional step" - ); - } - { - PhaseLock lock(mutex); - if (lock) { - nodes[i].initialized = false; - } - } - return markOptionalFailure(i, result, PhaseState::Starting); - } - PhaseLock lock(mutex); - if (lock) { - nodes[i].failed = true; - } - return result; - } - { - PhaseLock lock(mutex); - if (lock) { - nodes[i].started = true; - nodes[i].ready = true; - startOrder.push_back(i); - } - } - return result; - } - return PhaseResult::success(); - } - - PhaseResult runBoot() { - { - PhaseLock lock(mutex); - if (!lock) { - return PhaseResult::failure(PhaseStatus::InternalError, "lock failed"); - } - registrationClosed = true; - bootCount++; - } - PhaseResult validation = validateGraph(); - if (!validation) { - emitChange(PhaseState::Failed, PhaseNodeType::None, nullptr, validation.message, validation); - emitFailed(validation); - return validation; - } - resetRunState(); - - emitChange(PhaseState::Booting, PhaseNodeType::None, nullptr, "phase boot started"); - while (!allStepsInitialized() && !shouldStop() && !isEnding()) { - bool madeProgress = false; - PhaseResult result = runOneInitAction(madeProgress); - if (!result) { - rollback(); - emitChange(PhaseState::Failed, PhaseNodeType::None, nullptr, result.message, result); - emitFailed(result); - return result; - } - if (!madeProgress) { - PhaseResult stalled = - PhaseResult::failure(PhaseStatus::InternalError, "dependency graph stalled"); - rollback(); - emitChange(PhaseState::Failed, PhaseNodeType::None, nullptr, stalled.message, stalled); - emitFailed(stalled); - return stalled; - } - } - - if (shouldStop() || isEnding()) { - return runShutdown(); - } - - emitChange(PhaseState::Starting, PhaseNodeType::None, nullptr, "phase start/readiness started"); - while (!allNodesDone() && !shouldStop() && !isEnding()) { - bool madeProgress = false; - PhaseResult result = runOneReadinessAction(madeProgress); - if (!result) { - rollback(); - emitChange(PhaseState::Failed, PhaseNodeType::None, nullptr, result.message, result); - emitFailed(result); - return result; - } - if (!madeProgress) { - PhaseResult stalled = - PhaseResult::failure(PhaseStatus::InternalError, "dependency graph stalled"); - rollback(); - emitChange(PhaseState::Failed, PhaseNodeType::None, nullptr, stalled.message, stalled); - emitFailed(stalled); - return stalled; - } - } - - if (shouldStop() || isEnding()) { - return runShutdown(); - } - - emitChange(PhaseState::Ready, PhaseNodeType::None, nullptr, "phase ready"); - emitReady(); - return PhaseResult::success("phase ready"); - } - - PhaseResult rollback() { - { - PhaseLock lock(mutex); - if (lock) { - rollbackCount++; - } - } - stopStartedSteps(); - deinitInitializedSteps(); - resetGroups(); - return PhaseResult::success("rollback complete"); - } - - PhaseResult runShutdown() { - emitChange(PhaseState::Stopping, PhaseNodeType::None, nullptr, "phase stopping"); - stopStartedSteps(); - deinitInitializedSteps(); - resetGroups(); - { - PhaseLock lock(mutex); - if (lock) { - stopRequested = false; - } - } - emitChange(PhaseState::Stopped, PhaseNodeType::None, nullptr, "phase stopped"); - return PhaseResult::success("phase stopped"); - } - - void stopStartedSteps() { - std::vector order; - { - PhaseLock lock(mutex); - if (!lock) { - return; - } - order = startOrder; - } - for (auto it = order.rbegin(); it != order.rend(); ++it) { - PhaseNodeRuntimeSnapshot node; - PhaseStepCallbackSnapshot step; - if (!getNodeRuntimeSnapshot(*it, node) || !getStepCallbackSnapshot(*it, step)) { - continue; - } - if (!node.started) { - continue; - } - if (step.stopCallback) { - (void)runLifecycleCallback( - step.name, - step.type, - step.stopTimeoutMs, - step.stopCallback, - PhaseState::Stopping, - "stopping step", - true - ); - } - { - PhaseLock lock(mutex); - if (lock && *it < nodes.size()) { - nodes[*it].started = false; - nodes[*it].ready = false; - } - } - } - PhaseLock lock(mutex); - if (lock) { - startOrder.clear(); - } - } - - void deinitInitializedSteps() { - std::vector order; - { - PhaseLock lock(mutex); - if (!lock) { - return; - } - order = initOrder; - } - for (auto it = order.rbegin(); it != order.rend(); ++it) { - PhaseNodeRuntimeSnapshot node; - PhaseStepCallbackSnapshot step; - if (!getNodeRuntimeSnapshot(*it, node) || !getStepCallbackSnapshot(*it, step)) { - continue; - } - if (!node.initialized) { - continue; - } - if (step.deinitCallback) { - (void)runLifecycleCallback( - step.name, - step.type, - step.deinitTimeoutMs, - step.deinitCallback, - PhaseState::Deinitializing, - "deinitializing step", - true - ); - } - { - PhaseLock lock(mutex); - if (lock && *it < nodes.size()) { - nodes[*it].initialized = false; - nodes[*it].ready = false; - } - } - } - PhaseLock lock(mutex); - if (lock) { - initOrder.clear(); - } - } - - void resetGroups() { - PhaseLock lock(mutex); - if (!lock) { - return; - } - for (PhaseNode &node : nodes) { - if (node.type == PhaseNodeType::Group) { - node.ready = false; - } - } - } - - void taskLoop() { - { - PhaseLock lock(mutex); - if (lock) { - taskRunning = true; - } - } - if (taskStarted != nullptr) { - xSemaphoreGive(taskStarted); - } - while (!isEnding()) { - ulTaskNotifyTake(pdTRUE, portMAX_DELAY); - if (isEnding()) { - break; - } - bool localStop = false; - bool localStart = false; - { - PhaseLock lock(mutex); - if (lock) { - localStop = stopRequested; - localStart = startRequested; - startRequested = false; - if (localStart && !localStop) { - currentState = PhaseState::Booting; - } - } - } - if (localStop) { - (void)runShutdown(); - } - if (localStart && !localStop) { - (void)runBoot(); - } - } - if (shouldStop()) { - (void)runShutdown(); - } - { - PhaseLock lock(mutex); - if (lock) { - currentState = PhaseState::Ended; - taskRunning = false; - stackHighWaterMarkBytes = phase_task_support::currentStackHighWaterMarkBytes(); - taskHandle = nullptr; - } - } - phase_task_support::deleteCurrentTask(createdWithCaps); - } -}; - -PhaseResult PhaseResult::success(const char *message) { - return PhaseResult{true, PhaseStatus::Ok, message != nullptr ? message : "ok"}; -} - -PhaseResult PhaseResult::failure(PhaseStatus status, const char *message) { - return PhaseResult{false, status, message != nullptr ? message : "error"}; -} - -PhaseStepBuilder::PhaseStepBuilder(Phase *phase, size_t index, PhaseResult result) - : _phase(phase), _index(index), _result(result) { -} - -PhaseStepBuilder &PhaseStepBuilder::depends(const char *name) { - if (_result && _phase != nullptr) { - _result = _phase->addDependency(_index, name); - } - return *this; -} - -PhaseStepBuilder &PhaseStepBuilder::depends(std::initializer_list names) { - for (const char *name : names) { - depends(name); - if (!_result) { - break; - } - } - return *this; -} - -PhaseStepBuilder &PhaseStepBuilder::optional() { - if (_result && _phase != nullptr) { - _result = _phase->setOptional(_index); - } - return *this; -} - -PhaseStepBuilder &PhaseStepBuilder::initTimeout(uint32_t timeoutMs) { - if (_result && _phase != nullptr) { - _result = _phase->setStepTimeout(_index, kInitTimeout, timeoutMs); - } - return *this; -} - -PhaseStepBuilder &PhaseStepBuilder::startTimeout(uint32_t timeoutMs) { - if (_result && _phase != nullptr) { - _result = _phase->setStepTimeout(_index, kStartTimeout, timeoutMs); - } - return *this; -} - -PhaseStepBuilder &PhaseStepBuilder::stopTimeout(uint32_t timeoutMs) { - if (_result && _phase != nullptr) { - _result = _phase->setStepTimeout(_index, kStopTimeout, timeoutMs); - } - return *this; -} - -PhaseStepBuilder &PhaseStepBuilder::deinitTimeout(uint32_t timeoutMs) { - if (_result && _phase != nullptr) { - _result = _phase->setStepTimeout(_index, kDeinitTimeout, timeoutMs); - } - return *this; -} - -PhaseResult PhaseStepBuilder::setStartCallbacks( - PhaseCallback startCallback, - PhaseCallback stopCallback -) { - if (_phase == nullptr) { - return PhaseResult::failure(PhaseStatus::InternalError, "builder is not attached"); - } - return _phase->setStepStartCallbacks(_index, startCallback, stopCallback); -} - -PhaseGroupBuilder::PhaseGroupBuilder(Phase *phase, size_t index, PhaseResult result) - : _phase(phase), _index(index), _result(result) { -} - -PhaseGroupBuilder &PhaseGroupBuilder::depends(const char *name) { - if (_result && _phase != nullptr) { - _result = _phase->addDependency(_index, name); - } - return *this; -} - -PhaseGroupBuilder &PhaseGroupBuilder::depends(std::initializer_list names) { - for (const char *name : names) { - depends(name); - if (!_result) { - break; - } - } - return *this; -} - -PhaseGroupBuilder &PhaseGroupBuilder::optional() { - if (_result && _phase != nullptr) { - _result = _phase->setOptional(_index); - } - return *this; -} - -PhaseGroupBuilder &PhaseGroupBuilder::condition(PhaseConditionCallback callback) { - return condition(callback, 0); -} - -PhaseGroupBuilder &PhaseGroupBuilder::condition( - PhaseConditionCallback callback, - uint32_t timeoutMs -) { - if (_result && _phase != nullptr) { - _result = _phase->setGroupCondition(_index, callback, timeoutMs, timeoutMs > 0); - } - return *this; -} - -PhaseGroupBuilder &PhaseGroupBuilder::conditionPollInterval(uint32_t intervalMs) { - if (_result && _phase != nullptr) { - _result = _phase->setGroupPollInterval(_index, intervalMs); - } - return *this; -} - -Phase::Phase() : _impl(new (std::nothrow) PhaseImpl()) { -} - -Phase::~Phase() { - (void)end(0); -} - -PhaseResult Phase::init(const PhaseConfig &config) { - if (!_impl) { - return PhaseResult::failure(PhaseStatus::OutOfMemory, "phase allocation failed"); - } - SemaphoreHandle_t taskStarted = nullptr; - { - PhaseLock lock(_impl->mutex); - if (!lock) { - return PhaseResult::failure(PhaseStatus::InternalError, "lock failed"); - } - if (_impl->ending || _impl->currentState == PhaseState::Ended) { - return PhaseResult::failure(PhaseStatus::Busy, "phase has ended"); - } - if (_impl->initialized) { - return PhaseResult::failure(PhaseStatus::AlreadyInitialized, "phase is already initialized"); - } - if (config.maxNodes == 0 || config.maxDependenciesPerNode == 0) { - return PhaseResult::failure(PhaseStatus::InvalidArgument, "invalid phase limits"); - } - if (!phase_task_support::isValidStackSize(config.stackSizeBytes)) { - return PhaseResult::failure(PhaseStatus::InvalidArgument, "invalid stack size"); - } - if (_impl->taskStarted == nullptr) { - _impl->taskStarted = xSemaphoreCreateBinary(); - if (_impl->taskStarted == nullptr) { - return PhaseResult::failure(PhaseStatus::OutOfMemory, "phase task semaphore allocation failed"); - } - } else { - while (xSemaphoreTake(_impl->taskStarted, 0) == pdTRUE) { - } - } - - _impl->config = config; - _impl->actualStackType = PhaseStackType::Internal; - bool createdWithCaps = false; - bool usePsram = - config.stackType == PhaseStackType::Psram || - (config.stackType == PhaseStackType::Auto && phase_task_support::hasExternalStackSupport()); - BaseType_t created = phase_task_support::createTask( - PhaseImpl::taskEntry, - config.taskName, - config.stackSizeBytes, - _impl.get(), - config.priority, - &_impl->taskHandle, - config.coreId, - usePsram, - createdWithCaps - ); - if (created != pdPASS && config.stackType == PhaseStackType::Auto && usePsram) { - usePsram = false; - created = phase_task_support::createTask( - PhaseImpl::taskEntry, - config.taskName, - config.stackSizeBytes, - _impl.get(), - config.priority, - &_impl->taskHandle, - config.coreId, - false, - createdWithCaps - ); - } - if (created != pdPASS) { - _impl->taskHandle = nullptr; - return PhaseResult::failure(PhaseStatus::TaskCreateFailed, "phase task create failed"); - } - _impl->createdWithCaps = createdWithCaps; - _impl->actualStackType = - usePsram && createdWithCaps ? PhaseStackType::Psram : PhaseStackType::Internal; - _impl->initialized = true; - _impl->currentState = PhaseState::Idle; - taskStarted = _impl->taskStarted; - } - if (xSemaphoreTake(taskStarted, pdMS_TO_TICKS(kTaskStartTimeoutMs)) != pdTRUE) { - (void)end(kTaskStartTimeoutMs); - return PhaseResult::failure(PhaseStatus::Timeout, "phase task start timed out"); - } - return PhaseResult::success("phase initialized"); -} - -PhaseResult Phase::start() { - if (!_impl) { - return PhaseResult::failure(PhaseStatus::OutOfMemory, "phase allocation failed"); - } - { - PhaseLock lock(_impl->mutex); - if (!lock) { - return PhaseResult::failure(PhaseStatus::InternalError, "lock failed"); - } - if (_impl->ending) { - return PhaseResult::failure(PhaseStatus::Busy, "phase is ending"); - } - if (_impl->currentState == PhaseState::Ended) { - return PhaseResult::failure(PhaseStatus::Busy, "phase has ended"); - } - if (!_impl->initialized) { - return PhaseResult::failure(PhaseStatus::NotInitialized, "phase is not initialized"); - } - if (_impl->currentState != PhaseState::Idle && _impl->currentState != PhaseState::Stopped) { - if (_impl->currentState == PhaseState::Ready) { - return PhaseResult::failure(PhaseStatus::Busy, "phase is already ready"); - } - return PhaseResult::failure(PhaseStatus::Busy, "phase is busy"); - } - _impl->registrationClosed = true; - _impl->stopRequested = false; - _impl->startRequested = true; - } - return _impl->notifyTask(); -} - -PhaseResult Phase::stop() { - if (!_impl) { - return PhaseResult::failure(PhaseStatus::OutOfMemory, "phase allocation failed"); - } - { - PhaseLock lock(_impl->mutex); - if (!lock) { - return PhaseResult::failure(PhaseStatus::InternalError, "lock failed"); - } - if (_impl->ending) { - return PhaseResult::failure(PhaseStatus::Busy, "phase is ending"); - } - if (_impl->currentState == PhaseState::Ended) { - return PhaseResult::failure(PhaseStatus::Busy, "phase has ended"); - } - if (!_impl->initialized) { - return PhaseResult::failure(PhaseStatus::NotInitialized, "phase is not initialized"); - } - if (_impl->currentState == PhaseState::Idle || - _impl->currentState == PhaseState::Stopped) { - if (_impl->startRequested) { - _impl->startRequested = false; - return PhaseResult::success("phase start cancelled"); - } - return PhaseResult::success("phase stopped"); - } - if (_impl->currentState == PhaseState::Failed) { - return PhaseResult::success("phase stopped"); - } - if (_impl->currentState == PhaseState::Stopping || - _impl->currentState == PhaseState::Deinitializing) { - return PhaseResult::success("phase stopping"); - } - _impl->stopRequested = true; - } - return _impl->notifyTask(); -} - -PhaseResult Phase::end(uint32_t timeoutMs) { - if (!_impl) { - return PhaseResult::success(); - } - TaskHandle_t handle = nullptr; - { - PhaseLock lock(_impl->mutex); - if (!lock) { - return PhaseResult::failure(PhaseStatus::InternalError, "lock failed"); - } - if (!_impl->initialized) { - return PhaseResult::success("phase ended"); - } - _impl->ending = true; - _impl->stopRequested = true; - handle = _impl->taskHandle; - } - if (handle != nullptr) { - xTaskNotifyGive(handle); - } - const uint32_t startMs = millis(); - while (true) { - { - PhaseLock lock(_impl->mutex); - if (lock && !_impl->taskRunning && _impl->taskHandle == nullptr) { - _impl->initialized = false; - _impl->currentState = PhaseState::Ended; - return PhaseResult::success("phase ended"); - } - } - if (timeoutMs > 0 && millis() - startMs >= timeoutMs) { - return PhaseResult::failure(PhaseStatus::Timeout, "phase end timed out"); - } - vTaskDelay(pdMS_TO_TICKS(kWaitPollMs)); - } -} - -PhaseResult Phase::pause(const char *reason) { - if (!_impl) { - return PhaseResult::failure(PhaseStatus::OutOfMemory, "phase allocation failed"); - } - { - PhaseLock lock(_impl->mutex); - if (!lock) { - return PhaseResult::failure(PhaseStatus::InternalError, "lock failed"); - } - if (_impl->ending) { - return PhaseResult::failure(PhaseStatus::Busy, "phase is ending"); - } - if (_impl->currentState == PhaseState::Ended) { - return PhaseResult::failure(PhaseStatus::Busy, "phase has ended"); - } - if (!_impl->initialized) { - return PhaseResult::failure(PhaseStatus::NotInitialized, "phase is not initialized"); - } - _impl->paused = true; - _impl->pauseReason = reason != nullptr ? reason : ""; - } - return _impl->notifyTask(); -} - -PhaseResult Phase::resume() { - if (!_impl) { - return PhaseResult::failure(PhaseStatus::OutOfMemory, "phase allocation failed"); - } - { - PhaseLock lock(_impl->mutex); - if (!lock) { - return PhaseResult::failure(PhaseStatus::InternalError, "lock failed"); - } - if (_impl->ending) { - return PhaseResult::failure(PhaseStatus::Busy, "phase is ending"); - } - if (_impl->currentState == PhaseState::Ended) { - return PhaseResult::failure(PhaseStatus::Busy, "phase has ended"); - } - if (!_impl->initialized) { - return PhaseResult::failure(PhaseStatus::NotInitialized, "phase is not initialized"); - } - _impl->paused = false; - _impl->pauseReason.clear(); - } - return _impl->notifyTask(); -} - -bool Phase::isPaused() { - if (!_impl) { - return false; - } - PhaseLock lock(_impl->mutex); - return lock && _impl->paused; -} - -PhaseState Phase::state() { - if (!_impl) { - return PhaseState::Ended; - } - PhaseLock lock(_impl->mutex); - return lock ? _impl->currentState : PhaseState::Failed; -} - -PhaseDiag Phase::getDiagnostics() { - PhaseDiag diag; - if (!_impl) { - return diag; - } - PhaseLock lock(_impl->mutex); - if (!lock) { - return diag; - } - diag.nodeCount = _impl->nodes.size(); - for (const PhaseNode &node : _impl->nodes) { - if (node.initialized) { - diag.initializedCount++; - } - if (node.started) { - diag.startedCount++; - } - if (node.ready) { - diag.readyCount++; - } - if (node.failed) { - diag.failedCount++; - } - if (node.skipped) { - diag.skippedCount++; - } - } - diag.bootCount = _impl->bootCount; - diag.rollbackCount = _impl->rollbackCount; - diag.changeCount = _impl->changeCount; - diag.stackHighWaterMarkBytes = _impl->stackHighWaterMarkBytes; - diag.state = _impl->currentState; - diag.requestedStackType = _impl->config.stackType; - diag.actualStackType = _impl->actualStackType; - return diag; -} - -void Phase::onChange(PhaseChangeCallback callback) { - if (!_impl) { - return; - } - PhaseLock lock(_impl->mutex); - if (lock) { - _impl->changeCallback = callback; - } -} - -void Phase::onReady(PhaseReadyCallback callback) { - if (!_impl) { - return; - } - PhaseLock lock(_impl->mutex); - if (lock) { - _impl->readyCallback = callback; - } -} - -void Phase::onFailed(PhaseFailedCallback callback) { - if (!_impl) { - return; - } - PhaseLock lock(_impl->mutex); - if (lock) { - _impl->failedCallback = callback; - } -} - -PhaseStepBuilder Phase::addStep( - const char *name, - PhaseCallback initCallback, - PhaseCallback deinitCallback -) { - if (!_impl) { - return PhaseStepBuilder( - this, - 0, - PhaseResult::failure(PhaseStatus::OutOfMemory, "phase allocation failed") - ); - } - PhaseLock lock(_impl->mutex); - if (!lock) { - return PhaseStepBuilder( - this, - 0, - PhaseResult::failure(PhaseStatus::InternalError, "lock failed") - ); - } - PhaseResult open = _impl->validateRegistrationOpen(); - if (!open) { - return PhaseStepBuilder(this, 0, open); - } - if (name == nullptr || name[0] == '\0') { - return PhaseStepBuilder( - this, - 0, - PhaseResult::failure(PhaseStatus::InvalidArgument, "node name is required") - ); - } - if (!initCallback) { - return PhaseStepBuilder( - this, - 0, - PhaseResult::failure(PhaseStatus::InvalidCallback, "init callback is required") - ); - } - if (_impl->nodes.size() >= _impl->config.maxNodes) { - return PhaseStepBuilder( - this, - 0, - PhaseResult::failure(PhaseStatus::TooManyNodes, "too many nodes") - ); - } - if (_impl->findNodeIndex(name) < _impl->nodes.size()) { - return PhaseStepBuilder( - this, - 0, - PhaseResult::failure(PhaseStatus::DuplicateName, "duplicate node name") - ); - } - PhaseNode node; - node.type = PhaseNodeType::Step; - node.name = name; - node.initCallback = initCallback; - node.deinitCallback = deinitCallback; - _impl->nodes.push_back(node); - return PhaseStepBuilder(this, _impl->nodes.size() - 1, PhaseResult::success("step added")); -} - -PhaseGroupBuilder Phase::addGroup(const char *name) { - if (!_impl) { - return PhaseGroupBuilder( - this, - 0, - PhaseResult::failure(PhaseStatus::OutOfMemory, "phase allocation failed") - ); - } - PhaseLock lock(_impl->mutex); - if (!lock) { - return PhaseGroupBuilder( - this, - 0, - PhaseResult::failure(PhaseStatus::InternalError, "lock failed") - ); - } - PhaseResult open = _impl->validateRegistrationOpen(); - if (!open) { - return PhaseGroupBuilder(this, 0, open); - } - if (name == nullptr || name[0] == '\0') { - return PhaseGroupBuilder( - this, - 0, - PhaseResult::failure(PhaseStatus::InvalidArgument, "node name is required") - ); - } - if (_impl->nodes.size() >= _impl->config.maxNodes) { - return PhaseGroupBuilder( - this, - 0, - PhaseResult::failure(PhaseStatus::TooManyNodes, "too many nodes") - ); - } - if (_impl->findNodeIndex(name) < _impl->nodes.size()) { - return PhaseGroupBuilder( - this, - 0, - PhaseResult::failure(PhaseStatus::DuplicateName, "duplicate node name") - ); - } - PhaseNode node; - node.type = PhaseNodeType::Group; - node.name = name; - _impl->nodes.push_back(node); - return PhaseGroupBuilder(this, _impl->nodes.size() - 1, PhaseResult::success("group added")); -} - -PhaseResult Phase::addDependency(size_t index, const char *name) { - if (!_impl) { - return PhaseResult::failure(PhaseStatus::OutOfMemory, "phase allocation failed"); - } - PhaseLock lock(_impl->mutex); - if (!lock) { - return PhaseResult::failure(PhaseStatus::InternalError, "lock failed"); - } - PhaseResult open = _impl->validateRegistrationOpen(); - if (!open) { - return open; - } - if (index >= _impl->nodes.size()) { - return PhaseResult::failure(PhaseStatus::InvalidArgument, "invalid node"); - } - if (name == nullptr || name[0] == '\0') { - return PhaseResult::failure(PhaseStatus::InvalidArgument, "dependency name is required"); - } - PhaseNode &node = _impl->nodes[index]; - if (node.dependencies.size() >= _impl->config.maxDependenciesPerNode) { - return PhaseResult::failure(PhaseStatus::TooManyDependencies, "too many dependencies"); - } - if (std::find(node.dependencies.begin(), node.dependencies.end(), name) == node.dependencies.end()) { - node.dependencies.push_back(name); - } - return PhaseResult::success("dependency added"); -} - -PhaseResult Phase::setOptional(size_t index) { - if (!_impl) { - return PhaseResult::failure(PhaseStatus::OutOfMemory, "phase allocation failed"); - } - PhaseLock lock(_impl->mutex); - if (!lock) { - return PhaseResult::failure(PhaseStatus::InternalError, "lock failed"); - } - PhaseResult open = _impl->validateRegistrationOpen(); - if (!open) { - return open; - } - if (index >= _impl->nodes.size()) { - return PhaseResult::failure(PhaseStatus::InvalidArgument, "invalid node"); - } - _impl->nodes[index].optional = true; - return PhaseResult::success("node optional"); -} - -PhaseResult Phase::setStepStartCallbacks( - size_t index, - PhaseCallback startCallback, - PhaseCallback stopCallback -) { - if (!_impl) { - return PhaseResult::failure(PhaseStatus::OutOfMemory, "phase allocation failed"); - } - PhaseLock lock(_impl->mutex); - if (!lock) { - return PhaseResult::failure(PhaseStatus::InternalError, "lock failed"); - } - PhaseResult open = _impl->validateRegistrationOpen(); - if (!open) { - return open; - } - if (index >= _impl->nodes.size() || _impl->nodes[index].type != PhaseNodeType::Step) { - return PhaseResult::failure(PhaseStatus::InvalidArgument, "invalid step"); - } - if (!startCallback) { - return PhaseResult::failure(PhaseStatus::InvalidCallback, "start callback is required"); - } - _impl->nodes[index].startCallback = startCallback; - _impl->nodes[index].stopCallback = stopCallback; - return PhaseResult::success("start callback added"); -} - -PhaseResult Phase::setStepTimeout(size_t index, uint8_t timeoutKind, uint32_t timeoutMs) { - if (!_impl) { - return PhaseResult::failure(PhaseStatus::OutOfMemory, "phase allocation failed"); - } - PhaseLock lock(_impl->mutex); - if (!lock) { - return PhaseResult::failure(PhaseStatus::InternalError, "lock failed"); - } - PhaseResult open = _impl->validateRegistrationOpen(); - if (!open) { - return open; - } - if (index >= _impl->nodes.size() || _impl->nodes[index].type != PhaseNodeType::Step) { - return PhaseResult::failure(PhaseStatus::InvalidArgument, "invalid step"); - } - PhaseNode &node = _impl->nodes[index]; - switch (timeoutKind) { - case kInitTimeout: - node.hasInitTimeout = true; - node.initTimeoutMs = timeoutMs; - break; - case kStartTimeout: - node.hasStartTimeout = true; - node.startTimeoutMs = timeoutMs; - break; - case kStopTimeout: - node.hasStopTimeout = true; - node.stopTimeoutMs = timeoutMs; - break; - case kDeinitTimeout: - node.hasDeinitTimeout = true; - node.deinitTimeoutMs = timeoutMs; - break; - default: - return PhaseResult::failure(PhaseStatus::InvalidArgument, "invalid timeout kind"); - } - return PhaseResult::success("timeout set"); -} - -PhaseResult Phase::setGroupCondition( - size_t index, - PhaseConditionCallback callback, - uint32_t timeoutMs, - bool hasTimeoutOverride -) { - if (!_impl) { - return PhaseResult::failure(PhaseStatus::OutOfMemory, "phase allocation failed"); - } - PhaseLock lock(_impl->mutex); - if (!lock) { - return PhaseResult::failure(PhaseStatus::InternalError, "lock failed"); - } - PhaseResult open = _impl->validateRegistrationOpen(); - if (!open) { - return open; - } - if (index >= _impl->nodes.size() || _impl->nodes[index].type != PhaseNodeType::Group) { - return PhaseResult::failure(PhaseStatus::InvalidArgument, "invalid group"); - } - if (!callback) { - return PhaseResult::failure(PhaseStatus::InvalidCallback, "condition callback is required"); - } - PhaseNode &node = _impl->nodes[index]; - node.conditionCallback = callback; - node.hasGroupTimeout = hasTimeoutOverride; - node.groupTimeoutMs = timeoutMs; - return PhaseResult::success("condition set"); -} - -PhaseResult Phase::setGroupPollInterval(size_t index, uint32_t intervalMs) { - if (!_impl) { - return PhaseResult::failure(PhaseStatus::OutOfMemory, "phase allocation failed"); - } - PhaseLock lock(_impl->mutex); - if (!lock) { - return PhaseResult::failure(PhaseStatus::InternalError, "lock failed"); - } - PhaseResult open = _impl->validateRegistrationOpen(); - if (!open) { - return open; - } - if (index >= _impl->nodes.size() || _impl->nodes[index].type != PhaseNodeType::Group) { - return PhaseResult::failure(PhaseStatus::InvalidArgument, "invalid group"); - } - _impl->nodes[index].hasPollInterval = true; - _impl->nodes[index].pollIntervalMs = intervalMs; - return PhaseResult::success("poll interval set"); -} - -const char *Phase::statusToString(PhaseStatus status) const { - switch (status) { - case PhaseStatus::Ok: - return "Ok"; - case PhaseStatus::NotInitialized: - return "NotInitialized"; - case PhaseStatus::AlreadyInitialized: - return "AlreadyInitialized"; - case PhaseStatus::InvalidArgument: - return "InvalidArgument"; - case PhaseStatus::OutOfMemory: - return "OutOfMemory"; - case PhaseStatus::TaskCreateFailed: - return "TaskCreateFailed"; - case PhaseStatus::TooManyNodes: - return "TooManyNodes"; - case PhaseStatus::TooManyDependencies: - return "TooManyDependencies"; - case PhaseStatus::DuplicateName: - return "DuplicateName"; - case PhaseStatus::MissingDependency: - return "MissingDependency"; - case PhaseStatus::CircularDependency: - return "CircularDependency"; - case PhaseStatus::InvalidCallback: - return "InvalidCallback"; - case PhaseStatus::RegistrationClosed: - return "RegistrationClosed"; - case PhaseStatus::Busy: - return "Busy"; - case PhaseStatus::Timeout: - return "Timeout"; - case PhaseStatus::CallbackFailed: - return "CallbackFailed"; - case PhaseStatus::DependencyFailed: - return "DependencyFailed"; - case PhaseStatus::InternalError: - return "InternalError"; - default: - return "Unknown"; - } -} - -const char *Phase::stateToString(PhaseState state) const { - switch (state) { - case PhaseState::Idle: - return "Idle"; - case PhaseState::Booting: - return "Booting"; - case PhaseState::Starting: - return "Starting"; - case PhaseState::Ready: - return "Ready"; - case PhaseState::Paused: - return "Paused"; - case PhaseState::Stopping: - return "Stopping"; - case PhaseState::Deinitializing: - return "Deinitializing"; - case PhaseState::Stopped: - return "Stopped"; - case PhaseState::Failed: - return "Failed"; - case PhaseState::Ended: - return "Ended"; - default: - return "Unknown"; - } -} - -const char *Phase::nodeTypeToString(PhaseNodeType type) const { - switch (type) { - case PhaseNodeType::Step: - return "Step"; - case PhaseNodeType::Group: - return "Group"; - case PhaseNodeType::None: - return "None"; - default: - return "Unknown"; - } -} +// Phase is kept as one translation unit while the implementation is split into +// internal fragments to keep the lifecycle state machine reviewable. +#include "internal/PhaseRuntimeBase.inc" +#include "internal/PhaseRuntimeLifecycle.inc" +#include "internal/PhaseBuilders.inc" +#include "internal/PhaseInit.inc" +#include "internal/PhaseControl.inc" +#include "internal/PhaseRegistration.inc" diff --git a/src/internal/PhaseBuilders.inc b/src/internal/PhaseBuilders.inc new file mode 100644 index 0000000..1372485 --- /dev/null +++ b/src/internal/PhaseBuilders.inc @@ -0,0 +1,91 @@ +PhaseResult PhaseResult::success(const char *message) { + return PhaseResult{true, PhaseStatus::Ok, message != nullptr ? message : "ok"}; +} + +PhaseResult PhaseResult::failure(PhaseStatus status, const char *message) { + return PhaseResult{false, status, message != nullptr ? message : "error"}; +} + +PhaseStepBuilder::PhaseStepBuilder(Phase *phase, size_t index, PhaseResult result) + : _phase(phase), _index(index), _result(result) {} + +PhaseStepBuilder &PhaseStepBuilder::depends(const char *name) { + if (_result && _phase != nullptr) _result = _phase->addDependency(_index, name); + return *this; +} + +PhaseStepBuilder &PhaseStepBuilder::depends(std::initializer_list names) { + for (const char *name : names) { + depends(name); + if (!_result) break; + } + return *this; +} + +PhaseStepBuilder &PhaseStepBuilder::optional() { + if (_result && _phase != nullptr) _result = _phase->setOptional(_index); + return *this; +} + +PhaseStepBuilder &PhaseStepBuilder::initTimeout(uint32_t timeoutMs) { + if (_result && _phase != nullptr) _result = _phase->setStepTimeout(_index, kInitTimeout, timeoutMs); + return *this; +} + +PhaseStepBuilder &PhaseStepBuilder::startTimeout(uint32_t timeoutMs) { + if (_result && _phase != nullptr) _result = _phase->setStepTimeout(_index, kStartTimeout, timeoutMs); + return *this; +} + +PhaseStepBuilder &PhaseStepBuilder::stopTimeout(uint32_t timeoutMs) { + if (_result && _phase != nullptr) _result = _phase->setStepTimeout(_index, kStopTimeout, timeoutMs); + return *this; +} + +PhaseStepBuilder &PhaseStepBuilder::deinitTimeout(uint32_t timeoutMs) { + if (_result && _phase != nullptr) _result = _phase->setStepTimeout(_index, kDeinitTimeout, timeoutMs); + return *this; +} + +PhaseResult PhaseStepBuilder::setStartCallbacks(PhaseCallback startCallback, PhaseCallback stopCallback) { + if (_phase == nullptr) return PhaseResult::failure(PhaseStatus::InternalError, "builder is not attached"); + return _phase->setStepStartCallbacks(_index, std::move(startCallback), std::move(stopCallback)); +} + +PhaseGroupBuilder::PhaseGroupBuilder(Phase *phase, size_t index, PhaseResult result) + : _phase(phase), _index(index), _result(result) {} + +PhaseGroupBuilder &PhaseGroupBuilder::depends(const char *name) { + if (_result && _phase != nullptr) _result = _phase->addDependency(_index, name); + return *this; +} + +PhaseGroupBuilder &PhaseGroupBuilder::depends(std::initializer_list names) { + for (const char *name : names) { + depends(name); + if (!_result) break; + } + return *this; +} + +PhaseGroupBuilder &PhaseGroupBuilder::optional() { + if (_result && _phase != nullptr) _result = _phase->setOptional(_index); + return *this; +} + +PhaseGroupBuilder &PhaseGroupBuilder::condition(PhaseConditionCallback callback) { + return condition(std::move(callback), 0); +} + +PhaseGroupBuilder &PhaseGroupBuilder::condition(PhaseConditionCallback callback, uint32_t timeoutMs) { + if (_result && _phase != nullptr) { + _result = _phase->setGroupCondition(_index, std::move(callback), timeoutMs, timeoutMs > 0); + } + return *this; +} + +PhaseGroupBuilder &PhaseGroupBuilder::conditionPollInterval(uint32_t intervalMs) { + if (_result && _phase != nullptr) _result = _phase->setGroupPollInterval(_index, intervalMs); + return *this; +} + diff --git a/src/internal/PhaseControl.inc b/src/internal/PhaseControl.inc new file mode 100644 index 0000000..bcdcb53 --- /dev/null +++ b/src/internal/PhaseControl.inc @@ -0,0 +1,171 @@ +PhaseResult Phase::start() { + if (!_impl) return PhaseResult::failure(PhaseStatus::OutOfMemory, "phase allocation failed"); + { + PhaseLock lock(_impl->mutex); + if (!lock) return PhaseResult::failure(PhaseStatus::InternalError, "lock failed"); + if (_impl->ending) return PhaseResult::failure(PhaseStatus::Busy, "phase is ending"); + if (!_impl->initialized) return PhaseResult::failure(PhaseStatus::NotInitialized, "phase is not initialized"); + if (_impl->currentState == PhaseState::Ended) return PhaseResult::failure(PhaseStatus::Busy, "phase has ended"); + if (_impl->currentState != PhaseState::Idle && _impl->currentState != PhaseState::Stopped) { + return PhaseResult::failure(PhaseStatus::Busy, _impl->currentState == PhaseState::Ready ? "phase is already ready" : "phase is busy"); + } + if (!_impl->graphPrepared) { + PhaseResult graph = _impl->prepareGraphLocked(); + if (!graph) return graph; + } + _impl->registrationClosed = true; + _impl->stopRequested = false; + _impl->startRequested = true; + } + return _impl->notifyTask(); +} + +PhaseResult Phase::stop() { + if (!_impl) return PhaseResult::failure(PhaseStatus::OutOfMemory, "phase allocation failed"); + { + PhaseLock lock(_impl->mutex); + if (!lock) return PhaseResult::failure(PhaseStatus::InternalError, "lock failed"); + if (_impl->ending) return PhaseResult::failure(PhaseStatus::Busy, "phase is ending"); + if (!_impl->initialized) return PhaseResult::failure(PhaseStatus::NotInitialized, "phase is not initialized"); + if (_impl->currentState == PhaseState::Ended) return PhaseResult::failure(PhaseStatus::Busy, "phase has ended"); + if (_impl->currentState == PhaseState::Idle || _impl->currentState == PhaseState::Stopped) { + if (_impl->startRequested) { + _impl->startRequested = false; + return PhaseResult::success("phase start cancelled"); + } + return PhaseResult::success("phase stopped"); + } + if (_impl->currentState == PhaseState::Failed) return PhaseResult::success("phase stopped"); + if (_impl->currentState == PhaseState::Stopping || _impl->currentState == PhaseState::Deinitializing) { + return PhaseResult::success("phase stopping"); + } + _impl->stopRequested = true; + } + return _impl->notifyTask(); +} + +PhaseResult Phase::end(uint32_t timeoutMs) { + if (!_impl) return PhaseResult::success("phase ended"); + TaskHandle_t handle = nullptr; + SemaphoreHandle_t taskExited = nullptr; + { + PhaseLock lock(_impl->mutex); + if (!lock) return PhaseResult::failure(PhaseStatus::InternalError, "lock failed"); + if (!_impl->initialized) return PhaseResult::success("phase ended"); + if (_impl->taskHandle != nullptr && _impl->taskHandle == xTaskGetCurrentTaskHandle()) { + return PhaseResult::failure(PhaseStatus::Busy, "end cannot be called from the Phase task"); + } + _impl->ending = true; + _impl->stopRequested = true; + _impl->startRequested = false; + handle = _impl->taskHandle; + taskExited = _impl->taskExited; + } + if (handle != nullptr) xTaskNotifyGive(handle); + const uint32_t startMs = millis(); + while (true) { + { + PhaseLock lock(_impl->mutex); + if (lock && _impl->taskExitComplete) { + _impl->initialized = false; + _impl->currentState = PhaseState::Ended; + return PhaseResult::success("phase ended"); + } + } + if (timeoutMs > 0 && millis() - startMs >= timeoutMs) { + return PhaseResult::failure(PhaseStatus::Timeout, "phase end timed out"); + } + const TickType_t waitTicks = timeoutMs == 0 ? pdMS_TO_TICKS(kWaitPollMs) : + pdMS_TO_TICKS(std::min(kWaitPollMs, timeoutMs)); + if (taskExited != nullptr) (void)xSemaphoreTake(taskExited, waitTicks); + else vTaskDelay(waitTicks); + } +} + +PhaseResult Phase::pause(const char *reason) { + if (!_impl) return PhaseResult::failure(PhaseStatus::OutOfMemory, "phase allocation failed"); + { + PhaseLock lock(_impl->mutex); + if (!lock) return PhaseResult::failure(PhaseStatus::InternalError, "lock failed"); + if (_impl->ending) return PhaseResult::failure(PhaseStatus::Busy, "phase is ending"); + if (!_impl->initialized) return PhaseResult::failure(PhaseStatus::NotInitialized, "phase is not initialized"); + if (_impl->currentState == PhaseState::Ended) return PhaseResult::failure(PhaseStatus::Busy, "phase has ended"); + _impl->paused = true; + copyText(_impl->pauseReason.data(), _impl->pauseReason.size(), reason); + } + return _impl->notifyTask(); +} + +PhaseResult Phase::resume() { + if (!_impl) return PhaseResult::failure(PhaseStatus::OutOfMemory, "phase allocation failed"); + { + PhaseLock lock(_impl->mutex); + if (!lock) return PhaseResult::failure(PhaseStatus::InternalError, "lock failed"); + if (_impl->ending) return PhaseResult::failure(PhaseStatus::Busy, "phase is ending"); + if (!_impl->initialized) return PhaseResult::failure(PhaseStatus::NotInitialized, "phase is not initialized"); + if (_impl->currentState == PhaseState::Ended) return PhaseResult::failure(PhaseStatus::Busy, "phase has ended"); + _impl->paused = false; + _impl->pauseReason.fill('\0'); + } + return _impl->notifyTask(); +} + +bool Phase::isPaused() { + if (!_impl) return false; + PhaseLock lock(_impl->mutex); + return lock && _impl->paused; +} + +PhaseState Phase::state() { + if (!_impl) return PhaseState::Ended; + PhaseLock lock(_impl->mutex); + return lock ? _impl->currentState : PhaseState::Failed; +} + +PhaseDiag Phase::getDiagnostics() { + PhaseDiag diag; + if (!_impl) return diag; + PhaseLock lock(_impl->mutex); + if (!lock) return diag; + diag.nodeCount = _impl->nodes.size(); + for (const PhaseNode &node : _impl->nodes) { + if (node.initialized) diag.initializedCount++; + if (node.started) diag.startedCount++; + if (node.ready) diag.readyCount++; + if (node.failed) diag.failedCount++; + if (node.skipped) diag.skippedCount++; + } + diag.bootCount = _impl->bootCount; + diag.rollbackCount = _impl->rollbackCount; + diag.changeCount = _impl->changeCount; + diag.stackHighWaterMarkBytes = _impl->stackHighWaterMarkBytes; + diag.state = _impl->currentState; + diag.requestedStackType = _impl->config.stackType; + diag.actualStackType = _impl->actualStackType; + return diag; +} + +void Phase::onChange(PhaseChangeCallback callback) { + if (!_impl) return; + std::shared_ptr replacement; + if (callback) replacement = std::make_shared(std::move(callback)); + PhaseLock lock(_impl->mutex); + if (lock) _impl->changeCallback = std::move(replacement); +} + +void Phase::onReady(PhaseReadyCallback callback) { + if (!_impl) return; + std::shared_ptr replacement; + if (callback) replacement = std::make_shared(std::move(callback)); + PhaseLock lock(_impl->mutex); + if (lock) _impl->readyCallback = std::move(replacement); +} + +void Phase::onFailed(PhaseFailedCallback callback) { + if (!_impl) return; + std::shared_ptr replacement; + if (callback) replacement = std::make_shared(std::move(callback)); + PhaseLock lock(_impl->mutex); + if (lock) _impl->failedCallback = std::move(replacement); +} + diff --git a/src/internal/PhaseInit.inc b/src/internal/PhaseInit.inc new file mode 100644 index 0000000..fbc2c4d --- /dev/null +++ b/src/internal/PhaseInit.inc @@ -0,0 +1,112 @@ +Phase::Phase() : _impl(new (std::nothrow) PhaseImpl()) {} + +Phase::~Phase() { + if (!_impl) return; + bool calledFromPhaseTask = false; + { + PhaseLock lock(_impl->mutex); + if (lock && _impl->initialized && _impl->taskHandle == xTaskGetCurrentTaskHandle()) { + _impl->ending = true; + _impl->stopRequested = true; + _impl->startRequested = false; + _impl->deleteImplOnExit = true; + calledFromPhaseTask = true; + } + } + if (calledFromPhaseTask) { + (void)_impl.release(); + return; + } + (void)end(0); +} + +PhaseResult Phase::init(const PhaseConfig &config) { + if (!_impl) return PhaseResult::failure(PhaseStatus::OutOfMemory, "phase allocation failed"); + SemaphoreHandle_t taskStarted = nullptr; + { + PhaseLock lock(_impl->mutex); + if (!lock) return PhaseResult::failure(PhaseStatus::InternalError, "lock failed"); + if (_impl->ending || _impl->currentState == PhaseState::Ended) { + return PhaseResult::failure(PhaseStatus::Busy, "phase has ended"); + } + if (_impl->initialized) { + return PhaseResult::failure(PhaseStatus::AlreadyInitialized, "phase is already initialized"); + } + if (config.maxNodes == 0 || config.maxDependenciesPerNode == 0) { + return PhaseResult::failure(PhaseStatus::InvalidArgument, "invalid phase limits"); + } + if (!phase_task_support::isValidStackSize(config.stackSizeBytes)) { + return PhaseResult::failure(PhaseStatus::InvalidArgument, "invalid stack size"); + } + if (_impl->taskStarted == nullptr) _impl->taskStarted = xSemaphoreCreateBinary(); + if (_impl->taskExited == nullptr) _impl->taskExited = xSemaphoreCreateBinary(); + if (_impl->taskStarted == nullptr || _impl->taskExited == nullptr) { + return PhaseResult::failure(PhaseStatus::OutOfMemory, "phase semaphore allocation failed"); + } + while (xSemaphoreTake(_impl->taskStarted, 0) == pdTRUE) {} + while (xSemaphoreTake(_impl->taskExited, 0) == pdTRUE) {} + _impl->config = config; + _impl->nodes.clear(); + _impl->initOrder.clear(); + _impl->startOrder.clear(); + _impl->validationMarks.clear(); + _impl->nodes.reserve(config.maxNodes); + _impl->initOrder.reserve(config.maxNodes); + _impl->startOrder.reserve(config.maxNodes); + _impl->validationMarks.resize(config.maxNodes, 0); + _impl->actualStackType = PhaseStackType::Internal; + _impl->registrationClosed = false; + _impl->graphPrepared = false; + _impl->startRequested = false; + _impl->stopRequested = false; + _impl->ending = false; + _impl->taskRunning = false; + _impl->taskExitComplete = false; + _impl->paused = false; + _impl->deleteImplOnExit = false; + _impl->pauseReason.fill('\0'); + bool createdWithCaps = false; + bool usePsram = config.stackType == PhaseStackType::Psram || + (config.stackType == PhaseStackType::Auto && phase_task_support::hasExternalStackSupport()); + BaseType_t created = phase_task_support::createTask( + PhaseImpl::taskEntry, + config.taskName, + config.stackSizeBytes, + _impl.get(), + config.priority, + &_impl->taskHandle, + config.coreId, + usePsram, + createdWithCaps + ); + if (created != pdPASS && config.stackType == PhaseStackType::Auto && usePsram) { + usePsram = false; + created = phase_task_support::createTask( + PhaseImpl::taskEntry, + config.taskName, + config.stackSizeBytes, + _impl.get(), + config.priority, + &_impl->taskHandle, + config.coreId, + false, + createdWithCaps + ); + } + if (created != pdPASS) { + _impl->taskHandle = nullptr; + return PhaseResult::failure(PhaseStatus::TaskCreateFailed, "phase task create failed"); + } + _impl->createdWithCaps = createdWithCaps; + _impl->actualStackType = usePsram && createdWithCaps ? PhaseStackType::Psram : PhaseStackType::Internal; + _impl->initialized = true; + _impl->currentState = PhaseState::Idle; + taskStarted = _impl->taskStarted; + } + if (xSemaphoreTake(taskStarted, pdMS_TO_TICKS(kTaskStartTimeoutMs)) != pdTRUE) { + (void)end(kTaskStartTimeoutMs); + return PhaseResult::failure(PhaseStatus::Timeout, "phase task start timed out"); + } + return PhaseResult::success("phase initialized"); +} + diff --git a/src/internal/PhaseRegistration.inc b/src/internal/PhaseRegistration.inc new file mode 100644 index 0000000..652776c --- /dev/null +++ b/src/internal/PhaseRegistration.inc @@ -0,0 +1,210 @@ +PhaseStepBuilder Phase::addStep(const char *name, PhaseCallback initCallback, PhaseCallback deinitCallback) { + if (!_impl) { + return PhaseStepBuilder(this, 0, PhaseResult::failure(PhaseStatus::OutOfMemory, "phase allocation failed")); + } + PhaseLock lock(_impl->mutex); + if (!lock) return PhaseStepBuilder(this, 0, PhaseResult::failure(PhaseStatus::InternalError, "lock failed")); + PhaseResult open = _impl->validateRegistrationOpen(); + if (!open) return PhaseStepBuilder(this, 0, open); + if (name == nullptr || name[0] == '\0') { + return PhaseStepBuilder(this, 0, PhaseResult::failure(PhaseStatus::InvalidArgument, "node name is required")); + } + if (!initCallback) { + return PhaseStepBuilder(this, 0, PhaseResult::failure(PhaseStatus::InvalidCallback, "init callback is required")); + } + if (_impl->nodes.size() >= _impl->config.maxNodes) { + return PhaseStepBuilder(this, 0, PhaseResult::failure(PhaseStatus::TooManyNodes, "too many nodes")); + } + if (_impl->findNodeIndex(name) < _impl->nodes.size()) { + return PhaseStepBuilder(this, 0, PhaseResult::failure(PhaseStatus::DuplicateName, "duplicate node name")); + } + PhaseNode node; + node.type = PhaseNodeType::Step; + node.name = name; + node.dependencyNames.reserve(_impl->config.maxDependenciesPerNode); + node.dependencies.reserve(_impl->config.maxDependenciesPerNode); + node.initCallback = std::move(initCallback); + node.deinitCallback = std::move(deinitCallback); + _impl->nodes.push_back(std::move(node)); + _impl->graphPrepared = false; + return PhaseStepBuilder(this, _impl->nodes.size() - 1, PhaseResult::success("step added")); +} + +PhaseGroupBuilder Phase::addGroup(const char *name) { + if (!_impl) { + return PhaseGroupBuilder(this, 0, PhaseResult::failure(PhaseStatus::OutOfMemory, "phase allocation failed")); + } + PhaseLock lock(_impl->mutex); + if (!lock) return PhaseGroupBuilder(this, 0, PhaseResult::failure(PhaseStatus::InternalError, "lock failed")); + PhaseResult open = _impl->validateRegistrationOpen(); + if (!open) return PhaseGroupBuilder(this, 0, open); + if (name == nullptr || name[0] == '\0') { + return PhaseGroupBuilder(this, 0, PhaseResult::failure(PhaseStatus::InvalidArgument, "node name is required")); + } + if (_impl->nodes.size() >= _impl->config.maxNodes) { + return PhaseGroupBuilder(this, 0, PhaseResult::failure(PhaseStatus::TooManyNodes, "too many nodes")); + } + if (_impl->findNodeIndex(name) < _impl->nodes.size()) { + return PhaseGroupBuilder(this, 0, PhaseResult::failure(PhaseStatus::DuplicateName, "duplicate node name")); + } + PhaseNode node; + node.type = PhaseNodeType::Group; + node.name = name; + node.dependencyNames.reserve(_impl->config.maxDependenciesPerNode); + node.dependencies.reserve(_impl->config.maxDependenciesPerNode); + _impl->nodes.push_back(std::move(node)); + _impl->graphPrepared = false; + return PhaseGroupBuilder(this, _impl->nodes.size() - 1, PhaseResult::success("group added")); +} + +PhaseResult Phase::addDependency(size_t index, const char *name) { + if (!_impl) return PhaseResult::failure(PhaseStatus::OutOfMemory, "phase allocation failed"); + PhaseLock lock(_impl->mutex); + if (!lock) return PhaseResult::failure(PhaseStatus::InternalError, "lock failed"); + PhaseResult open = _impl->validateRegistrationOpen(); + if (!open) return open; + if (index >= _impl->nodes.size()) return PhaseResult::failure(PhaseStatus::InvalidArgument, "invalid node"); + if (name == nullptr || name[0] == '\0') { + return PhaseResult::failure(PhaseStatus::InvalidArgument, "dependency name is required"); + } + PhaseNode &node = _impl->nodes[index]; + if (node.dependencyNames.size() >= _impl->config.maxDependenciesPerNode) { + return PhaseResult::failure(PhaseStatus::TooManyDependencies, "too many dependencies"); + } + if (std::find(node.dependencyNames.begin(), node.dependencyNames.end(), name) == node.dependencyNames.end()) { + node.dependencyNames.emplace_back(name); + _impl->graphPrepared = false; + } + return PhaseResult::success("dependency added"); +} + +PhaseResult Phase::setOptional(size_t index) { + if (!_impl) return PhaseResult::failure(PhaseStatus::OutOfMemory, "phase allocation failed"); + PhaseLock lock(_impl->mutex); + if (!lock) return PhaseResult::failure(PhaseStatus::InternalError, "lock failed"); + PhaseResult open = _impl->validateRegistrationOpen(); + if (!open) return open; + if (index >= _impl->nodes.size()) return PhaseResult::failure(PhaseStatus::InvalidArgument, "invalid node"); + _impl->nodes[index].optional = true; + return PhaseResult::success("node optional"); +} + +PhaseResult Phase::setStepStartCallbacks(size_t index, PhaseCallback startCallback, PhaseCallback stopCallback) { + if (!_impl) return PhaseResult::failure(PhaseStatus::OutOfMemory, "phase allocation failed"); + PhaseLock lock(_impl->mutex); + if (!lock) return PhaseResult::failure(PhaseStatus::InternalError, "lock failed"); + PhaseResult open = _impl->validateRegistrationOpen(); + if (!open) return open; + if (index >= _impl->nodes.size() || _impl->nodes[index].type != PhaseNodeType::Step) { + return PhaseResult::failure(PhaseStatus::InvalidArgument, "invalid step"); + } + if (!startCallback) return PhaseResult::failure(PhaseStatus::InvalidCallback, "start callback is required"); + _impl->nodes[index].startCallback = std::move(startCallback); + _impl->nodes[index].stopCallback = std::move(stopCallback); + return PhaseResult::success("start callback added"); +} + +PhaseResult Phase::setStepTimeout(size_t index, uint8_t timeoutKind, uint32_t timeoutMs) { + if (!_impl) return PhaseResult::failure(PhaseStatus::OutOfMemory, "phase allocation failed"); + PhaseLock lock(_impl->mutex); + if (!lock) return PhaseResult::failure(PhaseStatus::InternalError, "lock failed"); + PhaseResult open = _impl->validateRegistrationOpen(); + if (!open) return open; + if (index >= _impl->nodes.size() || _impl->nodes[index].type != PhaseNodeType::Step) { + return PhaseResult::failure(PhaseStatus::InvalidArgument, "invalid step"); + } + PhaseNode &node = _impl->nodes[index]; + switch (timeoutKind) { + case kInitTimeout: node.hasInitTimeout = true; node.initTimeoutMs = timeoutMs; break; + case kStartTimeout: node.hasStartTimeout = true; node.startTimeoutMs = timeoutMs; break; + case kStopTimeout: node.hasStopTimeout = true; node.stopTimeoutMs = timeoutMs; break; + case kDeinitTimeout: node.hasDeinitTimeout = true; node.deinitTimeoutMs = timeoutMs; break; + default: return PhaseResult::failure(PhaseStatus::InvalidArgument, "invalid timeout kind"); + } + return PhaseResult::success("timeout set"); +} + +PhaseResult Phase::setGroupCondition( + size_t index, + PhaseConditionCallback callback, + uint32_t timeoutMs, + bool hasTimeoutOverride +) { + if (!_impl) return PhaseResult::failure(PhaseStatus::OutOfMemory, "phase allocation failed"); + PhaseLock lock(_impl->mutex); + if (!lock) return PhaseResult::failure(PhaseStatus::InternalError, "lock failed"); + PhaseResult open = _impl->validateRegistrationOpen(); + if (!open) return open; + if (index >= _impl->nodes.size() || _impl->nodes[index].type != PhaseNodeType::Group) { + return PhaseResult::failure(PhaseStatus::InvalidArgument, "invalid group"); + } + if (!callback) return PhaseResult::failure(PhaseStatus::InvalidCallback, "condition callback is required"); + PhaseNode &node = _impl->nodes[index]; + node.conditionCallback = std::move(callback); + node.hasGroupTimeout = hasTimeoutOverride; + node.groupTimeoutMs = timeoutMs; + return PhaseResult::success("condition set"); +} + +PhaseResult Phase::setGroupPollInterval(size_t index, uint32_t intervalMs) { + if (!_impl) return PhaseResult::failure(PhaseStatus::OutOfMemory, "phase allocation failed"); + PhaseLock lock(_impl->mutex); + if (!lock) return PhaseResult::failure(PhaseStatus::InternalError, "lock failed"); + PhaseResult open = _impl->validateRegistrationOpen(); + if (!open) return open; + if (index >= _impl->nodes.size() || _impl->nodes[index].type != PhaseNodeType::Group) { + return PhaseResult::failure(PhaseStatus::InvalidArgument, "invalid group"); + } + _impl->nodes[index].hasPollInterval = true; + _impl->nodes[index].pollIntervalMs = intervalMs; + return PhaseResult::success("poll interval set"); +} + +const char *Phase::statusToString(PhaseStatus status) const { + switch (status) { + case PhaseStatus::Ok: return "Ok"; + case PhaseStatus::NotInitialized: return "NotInitialized"; + case PhaseStatus::AlreadyInitialized: return "AlreadyInitialized"; + case PhaseStatus::InvalidArgument: return "InvalidArgument"; + case PhaseStatus::OutOfMemory: return "OutOfMemory"; + case PhaseStatus::TaskCreateFailed: return "TaskCreateFailed"; + case PhaseStatus::TooManyNodes: return "TooManyNodes"; + case PhaseStatus::TooManyDependencies: return "TooManyDependencies"; + case PhaseStatus::DuplicateName: return "DuplicateName"; + case PhaseStatus::MissingDependency: return "MissingDependency"; + case PhaseStatus::CircularDependency: return "CircularDependency"; + case PhaseStatus::InvalidCallback: return "InvalidCallback"; + case PhaseStatus::RegistrationClosed: return "RegistrationClosed"; + case PhaseStatus::Busy: return "Busy"; + case PhaseStatus::Timeout: return "Timeout"; + case PhaseStatus::CallbackFailed: return "CallbackFailed"; + case PhaseStatus::DependencyFailed: return "DependencyFailed"; + case PhaseStatus::InternalError: return "InternalError"; + default: return "Unknown"; + } +} + +const char *Phase::stateToString(PhaseState state) const { + switch (state) { + case PhaseState::Idle: return "Idle"; + case PhaseState::Booting: return "Booting"; + case PhaseState::Starting: return "Starting"; + case PhaseState::Ready: return "Ready"; + case PhaseState::Paused: return "Paused"; + case PhaseState::Stopping: return "Stopping"; + case PhaseState::Deinitializing: return "Deinitializing"; + case PhaseState::Stopped: return "Stopped"; + case PhaseState::Failed: return "Failed"; + case PhaseState::Ended: return "Ended"; + default: return "Unknown"; + } +} + +const char *Phase::nodeTypeToString(PhaseNodeType type) const { + switch (type) { + case PhaseNodeType::Step: return "Step"; + case PhaseNodeType::Group: return "Group"; + case PhaseNodeType::None: return "None"; + default: return "Unknown"; + } +} diff --git a/src/internal/PhaseRuntimeBase.inc b/src/internal/PhaseRuntimeBase.inc new file mode 100644 index 0000000..e3681c2 --- /dev/null +++ b/src/internal/PhaseRuntimeBase.inc @@ -0,0 +1,435 @@ +#include "Phase.h" + +#include "internal/PhaseMutex.h" +#include "internal/PhaseTaskSupport.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { +constexpr uint32_t kWaitPollMs = 10; +constexpr uint32_t kTaskStartTimeoutMs = 1000; +constexpr size_t kEventTextBytes = 192; +constexpr uint8_t kInitTimeout = 0; +constexpr uint8_t kStartTimeout = 1; +constexpr uint8_t kStopTimeout = 2; +constexpr uint8_t kDeinitTimeout = 3; + +void copyText(char *destination, size_t destinationSize, const char *source) { + if (destination == nullptr || destinationSize == 0) return; + if (source == nullptr) source = ""; + std::snprintf(destination, destinationSize, "%s", source); +} + +enum class DependencyState : uint8_t { + Ready, + Waiting, + FailedRequired, + SkipOptional, +}; + +struct PhaseNodeState { + PhaseNodeType type = PhaseNodeType::None; + bool optional = false; + bool initialized = false; + bool started = false; + bool ready = false; + bool failed = false; + bool skipped = false; + bool hasStartCallback = false; +}; +} // namespace + +struct PhaseNode { + PhaseNodeType type = PhaseNodeType::Step; + std::string name; + std::vector dependencyNames; + std::vector dependencies; + PhaseCallback initCallback; + PhaseCallback deinitCallback; + PhaseCallback startCallback; + PhaseCallback stopCallback; + PhaseConditionCallback conditionCallback; + bool optional = false; + bool initialized = false; + bool started = false; + bool ready = false; + bool failed = false; + bool skipped = false; + bool hasInitTimeout = false; + bool hasStartTimeout = false; + bool hasStopTimeout = false; + bool hasDeinitTimeout = false; + bool hasGroupTimeout = false; + bool hasPollInterval = false; + uint32_t initTimeoutMs = 0; + uint32_t startTimeoutMs = 0; + uint32_t stopTimeoutMs = 0; + uint32_t deinitTimeoutMs = 0; + uint32_t groupTimeoutMs = 0; + uint32_t pollIntervalMs = 0; +}; + +struct PhaseImpl { + PhaseConfig config{}; + PhaseMutex mutex; + std::vector nodes; + std::vector initOrder; + std::vector startOrder; + std::vector validationMarks; + std::shared_ptr changeCallback; + std::shared_ptr readyCallback; + std::shared_ptr failedCallback; + TaskHandle_t taskHandle = nullptr; + SemaphoreHandle_t taskStarted = nullptr; + SemaphoreHandle_t taskExited = nullptr; + bool createdWithCaps = false; + bool initialized = false; + bool registrationClosed = false; + bool graphPrepared = false; + bool startRequested = false; + bool stopRequested = false; + bool ending = false; + bool taskRunning = false; + bool taskExitComplete = false; + bool paused = false; + bool deleteImplOnExit = false; + std::array pauseReason{}; + PhaseState currentState = PhaseState::Idle; + PhaseStackType actualStackType = PhaseStackType::Internal; + uint32_t bootCount = 0; + uint32_t rollbackCount = 0; + uint32_t changeCount = 0; + size_t stackHighWaterMarkBytes = 0; + + ~PhaseImpl() { + if (taskStarted != nullptr) vSemaphoreDelete(taskStarted); + if (taskExited != nullptr) vSemaphoreDelete(taskExited); + } + + static void taskEntry(void *arg) { + static_cast(arg)->taskLoop(); + } + + PhaseResult notifyTask() { + TaskHandle_t handle = nullptr; + { + PhaseLock lock(mutex); + if (!lock) return PhaseResult::failure(PhaseStatus::InternalError, "lock failed"); + handle = taskHandle; + } + if (handle == nullptr) { + return PhaseResult::failure(PhaseStatus::NotInitialized, "phase task is not available"); + } + xTaskNotifyGive(handle); + return PhaseResult::success(); + } + + bool isEnding() { + PhaseLock lock(mutex); + return lock && ending; + } + + bool shouldStop() { + PhaseLock lock(mutex); + return lock && stopRequested; + } + + bool isPausedFlag() { + PhaseLock lock(mutex); + return lock && paused; + } + + void emitChange( + PhaseState state, + PhaseNodeType nodeType, + const char *nodeName, + const char *message, + PhaseResult result = PhaseResult::success(), + uint32_t durationMs = 0 + ) { + std::shared_ptr callback; + PhaseChange change; + std::array pauseReasonSnapshot{}; + std::array messageSnapshot{}; + { + PhaseLock lock(mutex); + if (!lock) return; + currentState = state; + changeCount++; + callback = changeCallback; + copyText(pauseReasonSnapshot.data(), pauseReasonSnapshot.size(), pauseReason.data()); + copyText( + messageSnapshot.data(), + messageSnapshot.size(), + message != nullptr ? message : result.message + ); + change.state = state; + change.nodeType = nodeType; + change.nodeName = nodeName; + change.pauseReason = pauseReasonSnapshot[0] == '\0' ? nullptr : pauseReasonSnapshot.data(); + change.message = messageSnapshot.data(); + change.isBooting = state == PhaseState::Booting; + change.isStarting = state == PhaseState::Starting; + change.isPaused = paused || state == PhaseState::Paused; + change.isStopping = state == PhaseState::Stopping; + change.isDeinitializing = state == PhaseState::Deinitializing; + change.isDone = state == PhaseState::Ready || state == PhaseState::Stopped || + state == PhaseState::Failed || state == PhaseState::Ended; + change.hasError = !result; + change.result = result; + change.result.message = messageSnapshot.data(); + change.durationMs = durationMs; + } + if (callback && *callback) (*callback)(change); + } + + void emitReady() { + std::shared_ptr callback; + { + PhaseLock lock(mutex); + if (!lock) return; + callback = readyCallback; + } + if (callback && *callback) (*callback)(); + } + + void emitFailed(PhaseResult result) { + std::shared_ptr callback; + std::array messageSnapshot{}; + { + PhaseLock lock(mutex); + if (!lock) return; + callback = failedCallback; + copyText(messageSnapshot.data(), messageSnapshot.size(), result.message); + result.message = messageSnapshot.data(); + } + if (callback && *callback) (*callback)(result); + } + + size_t findNodeIndex(const char *name) const { + if (name == nullptr) return nodes.size(); + for (size_t i = 0; i < nodes.size(); ++i) { + if (nodes[i].name == name) return i; + } + return nodes.size(); + } + + PhaseResult validateRegistrationOpen() const { + if (!initialized) { + return PhaseResult::failure(PhaseStatus::NotInitialized, "phase is not initialized"); + } + if (registrationClosed) { + return PhaseResult::failure(PhaseStatus::RegistrationClosed, "registration is closed after start"); + } + return PhaseResult::success(); + } + + bool hasCycle(size_t index) { + if (validationMarks[index] == 1) return true; + if (validationMarks[index] == 2) return false; + validationMarks[index] = 1; + for (size_t dependencyIndex : nodes[index].dependencies) { + if (hasCycle(dependencyIndex)) return true; + } + validationMarks[index] = 2; + return false; + } + + PhaseResult prepareGraphLocked() { + if (nodes.empty()) { + return PhaseResult::failure(PhaseStatus::InvalidArgument, "at least one node is required"); + } + for (size_t i = 0; i < nodes.size(); ++i) { + PhaseNode &node = nodes[i]; + if (node.name.empty()) { + return PhaseResult::failure(PhaseStatus::InvalidArgument, "node name is required"); + } + if (node.type == PhaseNodeType::Step && !node.initCallback) { + return PhaseResult::failure(PhaseStatus::InvalidCallback, "init callback is required"); + } + node.dependencies.clear(); + for (const std::string &dependencyName : node.dependencyNames) { + const size_t dependencyIndex = findNodeIndex(dependencyName.c_str()); + if (dependencyIndex >= nodes.size()) { + return PhaseResult::failure(PhaseStatus::MissingDependency, "dependency was not registered"); + } + node.dependencies.push_back(dependencyIndex); + } + } + std::fill(validationMarks.begin(), validationMarks.end(), 0); + for (size_t i = 0; i < nodes.size(); ++i) { + if (hasCycle(i)) { + return PhaseResult::failure(PhaseStatus::CircularDependency, "dependency cycle detected"); + } + } + graphPrepared = true; + return PhaseResult::success("dependency graph prepared"); + } + + bool getNodeState(size_t index, PhaseNodeState &out) { + PhaseLock lock(mutex); + if (!lock || index >= nodes.size()) return false; + const PhaseNode &node = nodes[index]; + out.type = node.type; + out.optional = node.optional; + out.initialized = node.initialized; + out.started = node.started; + out.ready = node.ready; + out.failed = node.failed; + out.skipped = node.skipped; + out.hasStartCallback = static_cast(node.startCallback); + return true; + } + + DependencyState dependencyState(size_t index, bool initWave) { + PhaseLock lock(mutex); + if (!lock || index >= nodes.size()) return DependencyState::FailedRequired; + const PhaseNode &node = nodes[index]; + for (size_t dependencyIndex : node.dependencies) { + if (dependencyIndex >= nodes.size()) return DependencyState::FailedRequired; + const PhaseNode &dependency = nodes[dependencyIndex]; + if (initWave && dependency.type == PhaseNodeType::Group) continue; + if (dependency.failed || dependency.skipped) { + return node.optional ? DependencyState::SkipOptional : DependencyState::FailedRequired; + } + if (initWave ? !dependency.initialized : !dependency.ready) { + return DependencyState::Waiting; + } + } + return DependencyState::Ready; + } + + bool allStepsInitialized() { + PhaseLock lock(mutex); + if (!lock) return false; + for (const PhaseNode &node : nodes) { + if (node.type == PhaseNodeType::Step && !node.initialized && !node.failed && !node.skipped) { + return false; + } + } + return true; + } + + bool allNodesDone() { + PhaseLock lock(mutex); + if (!lock) return false; + for (const PhaseNode &node : nodes) { + if (!node.ready && !node.failed && !node.skipped) return false; + } + return true; + } + + bool waitIfPaused(bool ignoreStop = false) { + if (ignoreStop) return true; + bool emitted = false; + PhaseState resumeState = PhaseState::Booting; + { + PhaseLock lock(mutex); + if (lock) resumeState = currentState; + } + while (isPausedFlag() && !shouldStop() && !isEnding()) { + if (!emitted) { + emitChange(PhaseState::Paused, PhaseNodeType::None, nullptr, "phase paused"); + emitted = true; + } + ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(kWaitPollMs)); + } + if (emitted && !shouldStop() && !isEnding()) { + emitChange(resumeState, PhaseNodeType::None, nullptr, "phase resumed"); + } + return !shouldStop() && !isEnding(); + } + + uint32_t resolveTimeout(const PhaseNode &node, uint8_t kind) const { + switch (kind) { + case kInitTimeout: return node.hasInitTimeout ? node.initTimeoutMs : config.defaultInitTimeoutMs; + case kStartTimeout: return node.hasStartTimeout ? node.startTimeoutMs : config.defaultStartTimeoutMs; + case kStopTimeout: return node.hasStopTimeout ? node.stopTimeoutMs : config.defaultStopTimeoutMs; + case kDeinitTimeout: return node.hasDeinitTimeout ? node.deinitTimeoutMs : config.defaultDeinitTimeoutMs; + default: return 0; + } + } + + PhaseResult runLifecycleCallback( + size_t index, + const PhaseCallback &callback, + uint32_t timeoutMs, + PhaseState state, + const char *startMessage, + bool ignoreStop = false + ) { + if (!callback) return PhaseResult::success(); + if (!waitIfPaused(ignoreStop)) return PhaseResult::failure(PhaseStatus::Busy, "phase stopped"); + const char *name = nodes[index].name.c_str(); + const PhaseNodeType type = nodes[index].type; + const uint32_t startMs = millis(); + emitChange(state, type, name, startMessage); + PhaseResult result = callback(); + const uint32_t elapsedMs = millis() - startMs; + if (result && timeoutMs > 0 && elapsedMs > timeoutMs) { + result = PhaseResult::failure(PhaseStatus::Timeout, "callback timed out"); + } + emitChange(state, type, name, result.message, result, elapsedMs); + return result; + } + + PhaseResult waitForGroup(size_t index) { + const PhaseNode &group = nodes[index]; + const uint32_t timeoutMs = group.hasGroupTimeout ? group.groupTimeoutMs : config.defaultGroupTimeoutMs; + const uint32_t pollMs = group.hasPollInterval ? group.pollIntervalMs : config.conditionPollIntervalMs; + if (!waitIfPaused()) return PhaseResult::failure(PhaseStatus::Busy, "phase stopped"); + uint32_t elapsedMs = 0; + emitChange(PhaseState::Starting, group.type, group.name.c_str(), "waiting for group"); + while (!shouldStop() && !isEnding()) { + if (!waitIfPaused()) return PhaseResult::failure(PhaseStatus::Busy, "phase stopped"); + if (!group.conditionCallback || group.conditionCallback()) { + emitChange(PhaseState::Starting, group.type, group.name.c_str(), "group ready"); + return PhaseResult::success("group ready"); + } + if (timeoutMs > 0 && elapsedMs >= timeoutMs) { + const PhaseResult result = PhaseResult::failure(PhaseStatus::Timeout, "group condition timed out"); + emitChange(PhaseState::Starting, group.type, group.name.c_str(), result.message, result); + return result; + } + const uint32_t delayMs = pollMs == 0 ? 1 : pollMs; + ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(delayMs)); + if (!isPausedFlag()) elapsedMs += delayMs; + } + return PhaseResult::failure(PhaseStatus::Busy, "phase stopped"); + } + + void resetRunState() { + PhaseLock lock(mutex); + if (!lock) return; + initOrder.clear(); + startOrder.clear(); + for (PhaseNode &node : nodes) { + node.initialized = false; + node.started = false; + node.ready = false; + node.failed = false; + node.skipped = false; + } + } + + PhaseResult markOptionalFailure(size_t index, PhaseResult result, PhaseState state) { + { + PhaseLock lock(mutex); + if (!lock || index >= nodes.size()) { + return PhaseResult::failure(PhaseStatus::InvalidArgument, "invalid node"); + } + nodes[index].failed = true; + nodes[index].ready = false; + } + emitChange(state, nodes[index].type, nodes[index].name.c_str(), result.message, result); + return PhaseResult::success("optional node failed"); + } + diff --git a/src/internal/PhaseRuntimeLifecycle.inc b/src/internal/PhaseRuntimeLifecycle.inc new file mode 100644 index 0000000..620c57c --- /dev/null +++ b/src/internal/PhaseRuntimeLifecycle.inc @@ -0,0 +1,331 @@ + PhaseResult runOneInitAction(bool &madeProgress) { + madeProgress = false; + for (size_t i = 0; i < nodes.size(); ++i) { + PhaseNodeState state; + if (!getNodeState(i, state)) continue; + if (state.type != PhaseNodeType::Step || state.initialized || state.failed || state.skipped) continue; + const DependencyState dependencies = dependencyState(i, true); + if (dependencies == DependencyState::Waiting) continue; + if (dependencies == DependencyState::SkipOptional) { + { + PhaseLock lock(mutex); + if (lock) nodes[i].skipped = true; + } + madeProgress = true; + emitChange(PhaseState::Booting, nodes[i].type, nodes[i].name.c_str(), "optional node skipped"); + return PhaseResult::success(); + } + if (dependencies == DependencyState::FailedRequired) { + { + PhaseLock lock(mutex); + if (lock) nodes[i].failed = true; + } + madeProgress = true; + return PhaseResult::failure(PhaseStatus::DependencyFailed, "required dependency failed"); + } + PhaseNode &node = nodes[i]; + PhaseResult result = runLifecycleCallback( + i, + node.initCallback, + resolveTimeout(node, kInitTimeout), + PhaseState::Booting, + "initializing step" + ); + madeProgress = true; + if (!result) { + if (state.optional) return markOptionalFailure(i, result, PhaseState::Booting); + PhaseLock lock(mutex); + if (lock) nodes[i].failed = true; + return result; + } + { + PhaseLock lock(mutex); + if (lock) { + nodes[i].initialized = true; + initOrder.push_back(i); + } + } + return result; + } + return PhaseResult::success(); + } + + PhaseResult runOneReadinessAction(bool &madeProgress) { + madeProgress = false; + for (size_t i = 0; i < nodes.size(); ++i) { + PhaseNodeState state; + if (!getNodeState(i, state)) continue; + if (state.ready || state.failed || state.skipped) continue; + const DependencyState dependencies = dependencyState(i, false); + if (dependencies == DependencyState::Waiting) continue; + if (dependencies == DependencyState::SkipOptional) { + { + PhaseLock lock(mutex); + if (lock) nodes[i].skipped = true; + } + madeProgress = true; + emitChange(PhaseState::Starting, nodes[i].type, nodes[i].name.c_str(), "optional node skipped"); + return PhaseResult::success(); + } + if (dependencies == DependencyState::FailedRequired) { + { + PhaseLock lock(mutex); + if (lock) nodes[i].failed = true; + } + madeProgress = true; + return PhaseResult::failure(PhaseStatus::DependencyFailed, "required dependency failed"); + } + PhaseNode &node = nodes[i]; + if (state.type == PhaseNodeType::Group) { + PhaseResult result = waitForGroup(i); + madeProgress = true; + if (result) { + PhaseLock lock(mutex); + if (lock) nodes[i].ready = true; + return result; + } + if (state.optional) return markOptionalFailure(i, result, PhaseState::Starting); + PhaseLock lock(mutex); + if (lock) nodes[i].failed = true; + return result; + } + if (!state.initialized) continue; + if (!state.hasStartCallback) { + { + PhaseLock lock(mutex); + if (lock) nodes[i].ready = true; + } + madeProgress = true; + emitChange(PhaseState::Starting, node.type, node.name.c_str(), "step ready"); + return PhaseResult::success("step ready"); + } + PhaseResult result = runLifecycleCallback( + i, + node.startCallback, + resolveTimeout(node, kStartTimeout), + PhaseState::Starting, + "starting step" + ); + madeProgress = true; + if (!result) { + if (state.optional) { + if (node.deinitCallback) { + (void)runLifecycleCallback( + i, + node.deinitCallback, + resolveTimeout(node, kDeinitTimeout), + PhaseState::Deinitializing, + "deinitializing optional step", + true + ); + } + { + PhaseLock lock(mutex); + if (lock) nodes[i].initialized = false; + } + return markOptionalFailure(i, result, PhaseState::Starting); + } + PhaseLock lock(mutex); + if (lock) nodes[i].failed = true; + return result; + } + { + PhaseLock lock(mutex); + if (lock) { + nodes[i].started = true; + nodes[i].ready = true; + startOrder.push_back(i); + } + } + return result; + } + return PhaseResult::success(); + } + + void stopStartedSteps() { + for (auto it = startOrder.rbegin(); it != startOrder.rend(); ++it) { + const size_t index = *it; + PhaseNodeState state; + if (!getNodeState(index, state) || !state.started) continue; + PhaseNode &node = nodes[index]; + if (node.stopCallback) { + (void)runLifecycleCallback( + index, + node.stopCallback, + resolveTimeout(node, kStopTimeout), + PhaseState::Stopping, + "stopping step", + true + ); + } + PhaseLock lock(mutex); + if (lock) { + nodes[index].started = false; + nodes[index].ready = false; + } + } + startOrder.clear(); + } + + void deinitInitializedSteps() { + for (auto it = initOrder.rbegin(); it != initOrder.rend(); ++it) { + const size_t index = *it; + PhaseNodeState state; + if (!getNodeState(index, state) || !state.initialized) continue; + PhaseNode &node = nodes[index]; + if (node.deinitCallback) { + (void)runLifecycleCallback( + index, + node.deinitCallback, + resolveTimeout(node, kDeinitTimeout), + PhaseState::Deinitializing, + "deinitializing step", + true + ); + } + PhaseLock lock(mutex); + if (lock) { + nodes[index].initialized = false; + nodes[index].ready = false; + } + } + initOrder.clear(); + } + + void resetGroups() { + PhaseLock lock(mutex); + if (!lock) return; + for (PhaseNode &node : nodes) { + if (node.type == PhaseNodeType::Group) node.ready = false; + } + } + + PhaseResult rollback() { + { + PhaseLock lock(mutex); + if (lock) rollbackCount++; + } + stopStartedSteps(); + deinitInitializedSteps(); + resetGroups(); + return PhaseResult::success("rollback complete"); + } + + PhaseResult runShutdown() { + emitChange(PhaseState::Stopping, PhaseNodeType::None, nullptr, "phase stopping"); + stopStartedSteps(); + deinitInitializedSteps(); + resetGroups(); + { + PhaseLock lock(mutex); + if (lock) stopRequested = false; + } + emitChange(PhaseState::Stopped, PhaseNodeType::None, nullptr, "phase stopped"); + return PhaseResult::success("phase stopped"); + } + + PhaseResult runBoot() { + { + PhaseLock lock(mutex); + if (!lock) return PhaseResult::failure(PhaseStatus::InternalError, "lock failed"); + bootCount++; + } + resetRunState(); + emitChange(PhaseState::Booting, PhaseNodeType::None, nullptr, "phase boot started"); + while (!allStepsInitialized() && !shouldStop() && !isEnding()) { + bool madeProgress = false; + PhaseResult result = runOneInitAction(madeProgress); + if (!result) { + rollback(); + emitChange(PhaseState::Failed, PhaseNodeType::None, nullptr, result.message, result); + emitFailed(result); + return result; + } + if (!madeProgress) { + PhaseResult stalled = PhaseResult::failure(PhaseStatus::InternalError, "dependency graph stalled"); + rollback(); + emitChange(PhaseState::Failed, PhaseNodeType::None, nullptr, stalled.message, stalled); + emitFailed(stalled); + return stalled; + } + } + if (shouldStop() || isEnding()) return runShutdown(); + emitChange(PhaseState::Starting, PhaseNodeType::None, nullptr, "phase start/readiness started"); + while (!allNodesDone() && !shouldStop() && !isEnding()) { + bool madeProgress = false; + PhaseResult result = runOneReadinessAction(madeProgress); + if (!result) { + rollback(); + emitChange(PhaseState::Failed, PhaseNodeType::None, nullptr, result.message, result); + emitFailed(result); + return result; + } + if (!madeProgress) { + PhaseResult stalled = PhaseResult::failure(PhaseStatus::InternalError, "dependency graph stalled"); + rollback(); + emitChange(PhaseState::Failed, PhaseNodeType::None, nullptr, stalled.message, stalled); + emitFailed(stalled); + return stalled; + } + } + if (shouldStop() || isEnding()) return runShutdown(); + emitChange(PhaseState::Ready, PhaseNodeType::None, nullptr, "phase ready"); + emitReady(); + return PhaseResult::success("phase ready"); + } + + void taskLoop() { + { + PhaseLock lock(mutex); + if (lock) taskRunning = true; + } + if (taskStarted != nullptr) xSemaphoreGive(taskStarted); + while (!isEnding()) { + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + if (isEnding()) break; + bool localStop = false; + bool localStart = false; + { + PhaseLock lock(mutex); + if (lock) { + localStop = stopRequested; + localStart = startRequested; + startRequested = false; + if (localStart && !localStop) currentState = PhaseState::Booting; + } + } + if (localStop) (void)runShutdown(); + if (localStart && !localStop) (void)runBoot(); + } + if (shouldStop()) (void)runShutdown(); + bool localCreatedWithCaps = false; + bool localDeleteImpl = false; + SemaphoreHandle_t localTaskExited = nullptr; + { + PhaseLock lock(mutex); + if (lock) { + currentState = PhaseState::Ended; + taskRunning = false; + stackHighWaterMarkBytes = phase_task_support::currentStackHighWaterMarkBytes(); + localCreatedWithCaps = createdWithCaps; + localDeleteImpl = deleteImplOnExit; + localTaskExited = taskExited; + } + } + if (localDeleteImpl) { + PhaseImpl *self = this; + delete self; + } else { + if (localTaskExited != nullptr) xSemaphoreGive(localTaskExited); + { + PhaseLock lock(mutex); + if (lock) { + taskExitComplete = true; + taskHandle = nullptr; + } + } + } + phase_task_support::deleteCurrentTask(localCreatedWithCaps); + } +}; + diff --git a/src/internal/PhaseTaskSupport.h b/src/internal/PhaseTaskSupport.h index 9b55d3d..da48fdb 100644 --- a/src/internal/PhaseTaskSupport.h +++ b/src/internal/PhaseTaskSupport.h @@ -18,7 +18,7 @@ extern "C" { #define PHASE_HAS_IDF_TASK_CAPS 0 #endif -#if PHASE_HAS_IDF_TASK_CAPS && defined(configSUPPORT_STATIC_ALLOCATION) && \ +#if PHASE_HAS_IDF_TASK_CAPS && defined(configSUPPORT_STATIC_ALLOCATION) && \ (configSUPPORT_STATIC_ALLOCATION == 1) && defined(MALLOC_CAP_SPIRAM) #define PHASE_CAN_USE_EXTERNAL_STACKS 1 #else @@ -48,7 +48,7 @@ inline bool isValidStackSize(size_t stackBytes) { inline size_t currentStackHighWaterMarkBytes() { #if defined(INCLUDE_uxTaskGetStackHighWaterMark) && (INCLUDE_uxTaskGetStackHighWaterMark == 1) - return static_cast(uxTaskGetStackHighWaterMark(nullptr)) * sizeof(StackType_t); + return static_cast(uxTaskGetStackHighWaterMark(nullptr)); #else return 0; #endif @@ -66,14 +66,10 @@ inline BaseType_t createTask( bool &createdWithCaps ) { createdWithCaps = false; - if (!isValidStackSize(stackBytes)) { - return pdFAIL; - } + if (!isValidStackSize(stackBytes)) return pdFAIL; if (usePsramStack) { #if PHASE_CAN_USE_EXTERNAL_STACKS - if (!hasExternalStackSupport()) { - return pdFAIL; - } + if (!hasExternalStackSupport()) return pdFAIL; const BaseType_t created = xTaskCreatePinnedToCoreWithCaps( entry, name, @@ -93,15 +89,7 @@ inline BaseType_t createTask( if (coreId == tskNO_AFFINITY) { return xTaskCreate(entry, name, static_cast(stackBytes), arg, priority, handle); } - return xTaskCreatePinnedToCore( - entry, - name, - static_cast(stackBytes), - arg, - priority, - handle, - coreId - ); + return xTaskCreatePinnedToCore(entry, name, static_cast(stackBytes), arg, priority, handle, coreId); } inline void deleteCurrentTask(bool withCaps) { @@ -110,6 +98,8 @@ inline void deleteCurrentTask(bool withCaps) { vTaskDeleteWithCaps(xTaskGetCurrentTaskHandle()); return; } +#else + (void)withCaps; #endif vTaskDelete(nullptr); } diff --git a/tests/host/semaphore_stubs.cpp b/tests/host/semaphore_stubs.cpp new file mode 100644 index 0000000..4383eb0 --- /dev/null +++ b/tests/host/semaphore_stubs.cpp @@ -0,0 +1,53 @@ +#include +#include +#include +#include +#include + +using Clock = std::chrono::steady_clock; +static const auto gStart = Clock::now(); +uint32_t millis() { + return static_cast(std::chrono::duration_cast(Clock::now() - gStart).count()); +} + +struct FakeSemaphore { + enum class Kind { Recursive, Binary } kind; + std::recursive_timed_mutex recursive; + std::mutex mutex; + std::condition_variable cv; + bool available = false; + explicit FakeSemaphore(Kind value) : kind(value) {} +}; + +SemaphoreHandle_t xSemaphoreCreateRecursiveMutex() { return new FakeSemaphore(FakeSemaphore::Kind::Recursive); } +SemaphoreHandle_t xSemaphoreCreateBinary() { return new FakeSemaphore(FakeSemaphore::Kind::Binary); } +BaseType_t xSemaphoreTakeRecursive(SemaphoreHandle_t handle, TickType_t timeout) { + if (!handle) return pdFALSE; + if (timeout == portMAX_DELAY) { handle->recursive.lock(); return pdTRUE; } + return handle->recursive.try_lock_for(std::chrono::milliseconds(timeout)) ? pdTRUE : pdFALSE; +} +BaseType_t xSemaphoreGiveRecursive(SemaphoreHandle_t handle) { + if (!handle) return pdFALSE; + handle->recursive.unlock(); + return pdTRUE; +} +BaseType_t xSemaphoreTake(SemaphoreHandle_t handle, TickType_t timeout) { + if (!handle) return pdFALSE; + std::unique_lock lock(handle->mutex); + if (timeout == 0) { + if (!handle->available) return pdFALSE; + } else if (timeout == portMAX_DELAY) { + handle->cv.wait(lock, [&] { return handle->available; }); + } else if (!handle->cv.wait_for(lock, std::chrono::milliseconds(timeout), [&] { return handle->available; })) { + return pdFALSE; + } + handle->available = false; + return pdTRUE; +} +BaseType_t xSemaphoreGive(SemaphoreHandle_t handle) { + if (!handle) return pdFALSE; + { std::lock_guard lock(handle->mutex); handle->available = true; } + handle->cv.notify_all(); + return pdTRUE; +} +void vSemaphoreDelete(SemaphoreHandle_t handle) { delete handle; } diff --git a/tests/host/stubs/Arduino.h b/tests/host/stubs/Arduino.h new file mode 100644 index 0000000..03c52a8 --- /dev/null +++ b/tests/host/stubs/Arduino.h @@ -0,0 +1,3 @@ +#pragma once +#include +uint32_t millis(); diff --git a/tests/host/stubs/esp_heap_caps.h b/tests/host/stubs/esp_heap_caps.h new file mode 100644 index 0000000..72533ce --- /dev/null +++ b/tests/host/stubs/esp_heap_caps.h @@ -0,0 +1,4 @@ +#pragma once +#include +#define MALLOC_CAP_8BIT 1 +inline size_t heap_caps_get_total_size(int) { return 0; } diff --git a/tests/host/stubs/freertos/FreeRTOS.h b/tests/host/stubs/freertos/FreeRTOS.h new file mode 100644 index 0000000..9a438da --- /dev/null +++ b/tests/host/stubs/freertos/FreeRTOS.h @@ -0,0 +1,16 @@ +#pragma once +#include +#include +using BaseType_t = int; +using UBaseType_t = unsigned int; +using TickType_t = uint32_t; +using StackType_t = uint32_t; +using configSTACK_DEPTH_TYPE = uint32_t; +#define pdTRUE 1 +#define pdFALSE 0 +#define pdPASS 1 +#define pdFAIL 0 +#define portMAX_DELAY UINT32_MAX +#define pdMS_TO_TICKS(ms) static_cast(ms) +#define tskNO_AFFINITY (-1) +#define INCLUDE_uxTaskGetStackHighWaterMark 1 diff --git a/tests/host/stubs/freertos/semphr.h b/tests/host/stubs/freertos/semphr.h new file mode 100644 index 0000000..a541a16 --- /dev/null +++ b/tests/host/stubs/freertos/semphr.h @@ -0,0 +1,11 @@ +#pragma once +#include "FreeRTOS.h" +struct FakeSemaphore; +using SemaphoreHandle_t = FakeSemaphore *; +SemaphoreHandle_t xSemaphoreCreateRecursiveMutex(); +SemaphoreHandle_t xSemaphoreCreateBinary(); +BaseType_t xSemaphoreTakeRecursive(SemaphoreHandle_t, TickType_t); +BaseType_t xSemaphoreGiveRecursive(SemaphoreHandle_t); +BaseType_t xSemaphoreTake(SemaphoreHandle_t, TickType_t); +BaseType_t xSemaphoreGive(SemaphoreHandle_t); +void vSemaphoreDelete(SemaphoreHandle_t); diff --git a/tests/host/stubs/freertos/task.h b/tests/host/stubs/freertos/task.h new file mode 100644 index 0000000..6cdf6c8 --- /dev/null +++ b/tests/host/stubs/freertos/task.h @@ -0,0 +1,13 @@ +#pragma once +#include "FreeRTOS.h" +struct FakeTask; +using TaskHandle_t = FakeTask *; +using TaskFunction_t = void (*)(void *); +BaseType_t xTaskCreate(TaskFunction_t, const char *, uint32_t, void *, UBaseType_t, TaskHandle_t *); +BaseType_t xTaskCreatePinnedToCore(TaskFunction_t, const char *, uint32_t, void *, UBaseType_t, TaskHandle_t *, BaseType_t); +void xTaskNotifyGive(TaskHandle_t); +uint32_t ulTaskNotifyTake(BaseType_t, TickType_t); +void vTaskDelay(TickType_t); +TaskHandle_t xTaskGetCurrentTaskHandle(); +void vTaskDelete(TaskHandle_t); +UBaseType_t uxTaskGetStackHighWaterMark(TaskHandle_t); diff --git a/tests/host/task_stubs.cpp b/tests/host/task_stubs.cpp new file mode 100644 index 0000000..0f268f2 --- /dev/null +++ b/tests/host/task_stubs.cpp @@ -0,0 +1,40 @@ +#include +#include +#include +#include +#include + +struct FakeTask { + std::mutex mutex; + std::condition_variable cv; + uint32_t notifications = 0; +}; +thread_local TaskHandle_t gCurrentTask = nullptr; +BaseType_t xTaskCreate(TaskFunction_t entry, const char *, uint32_t, void *arg, UBaseType_t, TaskHandle_t *out) { + auto *task = new FakeTask(); + *out = task; + std::thread([task, entry, arg] { gCurrentTask = task; entry(arg); gCurrentTask = nullptr; }).detach(); + return pdPASS; +} +BaseType_t xTaskCreatePinnedToCore(TaskFunction_t entry, const char *name, uint32_t stack, void *arg, UBaseType_t priority, TaskHandle_t *out, BaseType_t) { + return xTaskCreate(entry, name, stack, arg, priority, out); +} +void xTaskNotifyGive(TaskHandle_t task) { + if (!task) return; + { std::lock_guard lock(task->mutex); task->notifications++; } + task->cv.notify_all(); +} +uint32_t ulTaskNotifyTake(BaseType_t clear, TickType_t timeout) { + TaskHandle_t task = gCurrentTask; + if (!task) return 0; + std::unique_lock lock(task->mutex); + if (timeout == portMAX_DELAY) task->cv.wait(lock, [&] { return task->notifications > 0; }); + else if (!task->cv.wait_for(lock, std::chrono::milliseconds(timeout), [&] { return task->notifications > 0; })) return 0; + uint32_t value = task->notifications; + if (clear) task->notifications = 0; else task->notifications--; + return value; +} +void vTaskDelay(TickType_t ticks) { std::this_thread::sleep_for(std::chrono::milliseconds(ticks)); } +TaskHandle_t xTaskGetCurrentTaskHandle() { return gCurrentTask; } +void vTaskDelete(TaskHandle_t) {} +UBaseType_t uxTaskGetStackHighWaterMark(TaskHandle_t) { return 123; } diff --git a/tests/host/test_phase.cpp b/tests/host/test_phase.cpp new file mode 100644 index 0000000..57c4f98 --- /dev/null +++ b/tests/host/test_phase.cpp @@ -0,0 +1,204 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std::chrono_literals; + +std::atomic gTrackAllocations{false}; +std::atomic gTrackedAllocations{0}; + +void *operator new(std::size_t size) { + if (gTrackAllocations.load(std::memory_order_relaxed)) gTrackedAllocations.fetch_add(1, std::memory_order_relaxed); + if (void *memory = std::malloc(size)) return memory; + std::terminate(); +} + +void operator delete(void *memory) noexcept { std::free(memory); } +void operator delete(void *memory, std::size_t) noexcept { std::free(memory); } + +namespace { +void require(bool condition, const char *message) { + if (!condition) { + std::cerr << "FAIL: " << message << '\n'; + std::exit(1); + } +} + +bool waitForState(Phase &phase, PhaseState expected, uint32_t timeoutMs = 1000) { + const uint32_t started = millis(); + while (millis() - started < timeoutMs) { + if (phase.state() == expected) return true; + std::this_thread::sleep_for(1ms); + } + return phase.state() == expected; +} + +void testDependencyAndShutdownOrder() { + Phase phase; + require(static_cast(phase.init()), "init should succeed"); + std::mutex mutex; + std::vector calls; + auto addCall = [&](const char *value) { + std::lock_guard lock(mutex); + calls.emplace_back(value); + }; + + require(static_cast(phase.add("storage", [&] { addCall("init-storage"); }, [&] { addCall("deinit-storage"); }) + .start([&] { addCall("start-storage"); }, [&] { addCall("stop-storage"); })), + "storage registration should succeed"); + require(static_cast(phase.add("network", [&] { addCall("init-network"); }, [&] { addCall("deinit-network"); }) + .depends("storage") + .start([&] { addCall("start-network"); }, [&] { addCall("stop-network"); })), + "network registration should succeed"); + require(static_cast(phase.start()), "start should succeed"); + require(waitForState(phase, PhaseState::Ready), "phase should become ready"); + require(static_cast(phase.stop()), "stop should succeed"); + require(waitForState(phase, PhaseState::Stopped), "phase should stop"); + + const std::vector expected = { + "init-storage", "init-network", "start-storage", "start-network", + "stop-network", "stop-storage", "deinit-network", "deinit-storage" + }; + { + std::lock_guard lock(mutex); + require(calls == expected, "lifecycle ordering should be dependency/reverse dependency ordered"); + } + require(static_cast(phase.end()), "end should succeed"); + require(phase.getDiagnostics().stackHighWaterMarkBytes == 123, "stack high-water mark should remain byte-valued"); +} + +void testFailureRollback() { + Phase phase; + require(static_cast(phase.init()), "init should succeed"); + std::mutex mutex; + std::vector calls; + auto addCall = [&](const char *value) { + std::lock_guard lock(mutex); + calls.emplace_back(value); + }; + phase.add("one", [&] { addCall("init-one"); }, [&] { addCall("deinit-one"); }); + phase.add("two", [&]() -> PhaseResult { + addCall("init-two"); + return PhaseResult::failure(PhaseStatus::CallbackFailed, "boom"); + }, [&] { addCall("deinit-two"); }).depends("one"); + require(static_cast(phase.start()), "start request should succeed"); + require(waitForState(phase, PhaseState::Failed), "phase should fail"); + { + std::lock_guard lock(mutex); + const std::vector expected = {"init-one", "init-two", "deinit-one"}; + require(calls == expected, "rollback should deinitialize only initialized steps"); + } + require(phase.getDiagnostics().rollbackCount == 1, "rollback should be counted"); + require(static_cast(phase.end()), "end should succeed after failure"); +} + +void testOptionalAndGroupPause() { + Phase phase; + require(static_cast(phase.init()), "init should succeed"); + phase.add("optional", [] { return false; }).optional(); + phase.add("dependent", [] {}).depends("optional").optional(); + std::atomic gate{false}; + phase.addGroup("gate").condition([&] { return gate.load(); }, 500).conditionPollInterval(2); + require(static_cast(phase.pause("maintenance")), "pause should succeed"); + require(static_cast(phase.start()), "start should succeed"); + require(waitForState(phase, PhaseState::Paused), "phase should pause before lifecycle work"); + gate = true; + require(static_cast(phase.resume()), "resume should succeed"); + require(waitForState(phase, PhaseState::Ready), "optional failures should not block ready"); + PhaseDiag diag = phase.getDiagnostics(); + require(diag.failedCount == 1, "optional failure should be recorded"); + require(diag.skippedCount == 1, "optional dependent should be skipped"); + require(static_cast(phase.end()), "end should succeed"); +} + +void testCallbackSafety() { + Phase phase; + require(static_cast(phase.init()), "init should succeed"); + phase.add("app", [] {}); + std::atomic pausedCallbackEntered{false}; + std::atomic pauseReasonStable{false}; + std::atomic endRejected{false}; + phase.onChange([&](PhaseChange change) { + if (change.state == PhaseState::Paused && change.pauseReason != nullptr) { + const std::string before = change.pauseReason; + pausedCallbackEntered = true; + std::this_thread::sleep_for(20ms); + pauseReasonStable = before == change.pauseReason; + } + }); + phase.onReady([&] { + PhaseResult result = phase.end(20); + endRejected = !result && result.status == PhaseStatus::Busy; + }); + require(static_cast(phase.pause("snapshot-reason")), "pause should succeed"); + require(static_cast(phase.start()), "start should succeed"); + const uint32_t started = millis(); + while (!pausedCallbackEntered && millis() - started < 500) std::this_thread::sleep_for(1ms); + require(pausedCallbackEntered, "paused callback should run"); + require(static_cast(phase.resume()), "resume should succeed concurrently with callback"); + require(waitForState(phase, PhaseState::Ready), "phase should become ready"); + require(pauseReasonStable, "pause reason pointer should stay valid during callback"); + require(endRejected, "end should be rejected from the Phase task"); + require(static_cast(phase.end()), "external end should succeed"); +} + +void testLifecycleDoesNotAllocate() { + Phase phase; + require(static_cast(phase.init()), "init should succeed"); + phase.add("root", [] {}).start([] {}, [] {}); + phase.add("dependent", [] {}).depends("root").start([] {}, [] {}); + gTrackedAllocations = 0; + gTrackAllocations = true; + require(static_cast(phase.start()), "start should succeed"); + require(waitForState(phase, PhaseState::Ready), "phase should become ready"); + require(static_cast(phase.stop()), "stop should succeed"); + require(waitForState(phase, PhaseState::Stopped), "phase should stop"); + gTrackAllocations = false; + require(gTrackedAllocations == 0, "lifecycle execution should not allocate"); + require(static_cast(phase.end()), "end should succeed"); +} + +void testPendingStartCancellationAndRestart() { + Phase phase; + require(static_cast(phase.init()), "init should succeed"); + std::atomic initCount{0}; + phase.add("app", [&] { initCount++; }); + require(static_cast(phase.start()), "start should succeed"); + PhaseResult stopped = phase.stop(); + require(static_cast(stopped), "immediate stop should succeed"); + std::this_thread::sleep_for(20ms); + if (phase.state() == PhaseState::Stopped) { + require(static_cast(phase.start()), "restart should succeed"); + require(waitForState(phase, PhaseState::Ready), "restart should become ready"); + } else { + require(phase.state() == PhaseState::Idle, "cancelled start should remain idle"); + require(initCount == 0, "cancelled pending start should not initialize"); + require(static_cast(phase.start()), "start after cancellation should succeed"); + require(waitForState(phase, PhaseState::Ready), "start after cancellation should become ready"); + } + require(static_cast(phase.stop()), "stop should succeed"); + require(waitForState(phase, PhaseState::Stopped), "phase should stop"); + require(static_cast(phase.start()), "restart from stopped should succeed"); + require(waitForState(phase, PhaseState::Ready), "restart from stopped should become ready"); + require(static_cast(phase.end()), "end should succeed"); +} +} + +int main() { + testDependencyAndShutdownOrder(); + testFailureRollback(); + testOptionalAndGroupPause(); + testCallbackSafety(); + testLifecycleDoesNotAllocate(); + testPendingStartCancellationAndRestart(); + std::cout << "Phase host tests passed\n"; + return 0; +}