diff --git a/.github/workflows/host-tests.yml b/.github/workflows/host-tests.yml index 8a89fc1..c216287 100644 --- a/.github/workflows/host-tests.yml +++ b/.github/workflows/host-tests.yml @@ -2,7 +2,7 @@ name: Host Tests on: push: - branches: [main, bugfix/7-event-monitor-structured-payload-safety, feature/8-dependency-refresh-0.5.1] + branches: [main, bugfix/10-console-poll-heap-pressure] pull_request: jobs: @@ -144,6 +144,7 @@ jobs: cat > project/compile/src/main.cpp <<'EOF' #include #include + #include #include #include #include @@ -152,8 +153,15 @@ jobs: #include #include - void setup() {} + ESPressio::Serial::Console console; + + void setup() { + ESPressio::Serial::ConsoleConfig config; + config.ShowPrompt = false; + config.MaximumLineLength = 128; + console.Initialize(Serial, Serial, config); + } void loop() {} EOF - - name: Compile ESP32 Observable and Event monitors + - name: Compile ESP32 Console and Observable/Event monitors run: pio run -d project/compile diff --git a/CHANGELOG.md b/CHANGELOG.md index ff1bf76..c101f8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +## 0.5.2 + +### Fixed + +- Fixed a heap-pressure failure path in `Console::Poll()` where the bounded input line still grew its backing `std::string` incrementally and could reach `std::terminate()` if a late `push_back()` allocation failed. +- `Console::Initialize()` now reserves the configured `MaximumLineLength` capacity before publishing the console as initialized. +- If the bounded input capacity cannot be reserved, console initialization now fails cleanly instead of leaving a partially initialized console that can fail later while accepting input. +- Corrected `component.mk` version macros that had remained at 0.5.0 after the 0.5.1 release. + +### Tests + +- Added regression coverage confirming that the configured input capacity is established during initialization and is retained across normal polling, buffer clearing, maximum-length input, and over-length discard handling. +- The test-only capacity accessor is compiled only under `ESPRESSIO_SERIAL_TESTING` and does not alter the production API surface. + +### Compatibility + +- No breaking public API changes. +- Existing maximum-line-length, discard, prompt, echo, command, and interceptor behavior is unchanged. +- Core Serial remains dependency-free. + ## 0.5.1 ### Fixed @@ -104,7 +124,6 @@ The structure follows the principles of [Keep a Changelog](https://keepachangelo - Added optional pre-dispatch operator confirmation. - Added safe-by-default Event dispatch authorization using allow-list, allow-all, and deny-list policies; deny-list entries override broader access. - Added optional `ILoggerSink` audit integration for operator dispatch, denial, malformed JSON, and construction/dispatch failures. -- Added `Console`, `EventConsole`, and `EventConsoleLoopback` examples. - Added host-side `Console` tests and an `EventConsole` contract test covering discovery, description, access policy, JSON command handling, confirmation, and dispatch. - Added the 0.3.0 feature specification. diff --git a/ESPRESSIO_DEPENDENCY_CHART.md b/ESPRESSIO_DEPENDENCY_CHART.md index 491818b..26ef535 100644 --- a/ESPRESSIO_DEPENDENCY_CHART.md +++ b/ESPRESSIO_DEPENDENCY_CHART.md @@ -1,8 +1,8 @@ -# ESPressio Dependency Chart — Serial 0.5.1 +# ESPressio Dependency Chart — Serial 0.5.2 ![ESPressio Library Dependency Chart](ESPRESSIO_DEPENDENCY_CHART.png) -## ESPressio Serial 0.5.1 +## ESPressio Serial 0.5.2 The Serial core and generic `Console` have no mandatory ESPressio dependencies. All ESPressio integrations remain opt-in. @@ -37,9 +37,10 @@ EventMonitor / EventConsole - - -> ESPressio Serializable >= 0.10.2 < 1.0.0 ``` -EventMonitor 0.5.1 uses Serializable 0.10.2's bounded, allocation-free ESPB -traversal API for structured diagnostics. Serializable 0.10.2 also contains the -strict-build warning correction required by Serial's `-Werror` host validation. +EventMonitor 0.5.2 retains Serializable 0.10.2's bounded, allocation-free ESPB +traversal API for structured diagnostics. Serial 0.5.2 additionally reserves +the generic Console's configured bounded input capacity during initialization, +removing avoidable line-buffer growth allocations from `Console::Poll()`. ## Current coordinated ecosystem @@ -73,7 +74,7 @@ EVENT └── Serializable >= 0.10.2 < 1.0.0 [optional] DIAGNOSTICS / OPERATOR -└── Serial 0.5.1 +└── Serial 0.5.2 ``` ## Dependency-direction rule diff --git a/README.md b/README.md index 07a84ab..06fb1d5 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,13 @@ Serial and console-oriented components for the Flowduino ESPressio Development Platform. -Version 0.5.1 hardens the opt-in Event Monitor so structured Event Transport diagnostics are rendered directly from bounded ESPB bytes without constructing a second heap-backed `SerializationNode` tree. It retains the Observable-backed monitors introduced in 0.5.0 while preserving the dependency-free Serial core and existing Console/Event integrations. +Version 0.5.2 hardens the generic Console under heap pressure by reserving its configured bounded input capacity during initialization, so normal line polling no longer grows the backing `std::string` incrementally. It retains the EventMonitor safety work from 0.5.1 and the Observable-backed monitors introduced in 0.5.0 while preserving the dependency-free Serial core. -## Current Version — 0.5.1 +## Current Version — 0.5.2 -Version **0.5.1** fixes the structured EventMonitor crash path reproduced on ESP32 under low-memory conditions. Structured diagnostics now use ESPressio Serializable 0.10.2's allocation-free BinaryArchive traversal API with explicit depth, aggregate-node, collection, name, and string limits. Invalid or outside-limit payloads fall back to bounded hexadecimal output rather than becoming fatal diagnostic work. +Version **0.5.2** fixes a low-memory failure path reproduced on ESP32 where `Console::Poll()` could reach `std::terminate()` when `std::string::push_back()` needed to grow the input buffer after the system was already under severe heap pressure. `Console::Initialize()` now prepares the configuration and reserves `ConsoleConfig::MaximumLineLength` before publishing the console as initialized. If that bounded capacity cannot be reserved, initialization returns `false` cleanly. + +The EventMonitor structured-payload hardening from 0.5.1 remains unchanged: structured diagnostics use ESPressio Serializable 0.10.2's allocation-free BinaryArchive traversal API with explicit depth, aggregate-node, collection, name, and string limits. Invalid or outside-limit payloads fall back to bounded hexadecimal output rather than becoming fatal diagnostic work. The Observable-backed monitor integrations introduced in 0.5.0 remain available unchanged: @@ -34,7 +36,7 @@ These monitors subscribe directly to the originating library's Observable contra Historical documentation for earlier release generations remains below where useful. -Current coordinated dependency baselines for the 0.5.1 release are Units 0.2.3, Timing 2.2.4, Threads 3.1.4, ESP-Now 0.5.2, Event 5.8.2, and Serializable 0.10.2. Command 0.3.0, Security 0.2.0, and Sockets 0.5.0 remain the current optional integration baselines. +Current coordinated dependency baselines for the 0.5.2 release are Units 0.2.3, Timing 2.2.4, Threads 3.1.4, ESP-Now 0.5.2, Event 5.8.2, and Serializable 0.10.2. Command 0.3.0, Security 0.2.0, and Sockets 0.5.0 remain the current optional integration baselines. ## ESPressio Development Platform @@ -163,11 +165,14 @@ void setup() { ESPressio::Serial::ConsoleConfig config; config.Prompt = "espressio> "; - console.Initialize( + if (!console.Initialize( ::Serial, ::Serial, config - ); + )) { + // The bounded input buffer could not be reserved. + return; + } console.RegisterCommand( "hello", @@ -193,7 +198,9 @@ The line buffer is bounded through: ConsoleConfig::MaximumLineLength ``` -and the console supports: +Beginning with 0.5.2, that bounded capacity is reserved during `Initialize()`. A successful initialization therefore guarantees that ordinary input up to `MaximumLineLength` does not need to grow the backing line buffer while `Poll()` is running. If the reservation cannot be satisfied, initialization returns `false` and the console remains uninitialized. + +The console supports: ```text command registration @@ -520,6 +527,9 @@ argument preservation multiple interactive line interceptors interceptor removal Stream polling +bounded Console input capacity reservation +capacity retention across polling and line clearing +over-length discard handling without buffer growth runtime Event listing Event schema description allow-list enforcement @@ -880,7 +890,7 @@ config.MaximumHexPayloadBytes ESPressio Event Transport serializes Event payloads using ESPressio Serializable's BinaryArchive ESPB v2 representation. -Beginning with Serial 0.5.1, EventMonitor does **not** decode that payload into a second `SerializationNode` tree merely for presentation. Instead, it uses Serializable 0.10.1's `TraverseBinaryArchive()` API to validate and stream the existing ESPB bytes directly to the selected Arduino `Print` destination. +Beginning with Serial 0.5.1, EventMonitor does **not** decode that payload into a second `SerializationNode` tree merely for presentation. Instead, it uses Serializable 0.10.2's `TraverseBinaryArchive()` API to validate and stream the existing ESPB bytes directly to the selected Arduino `Print` destination. This keeps human-readable structured diagnostics independent of the concrete C++ Event type and avoids ArduinoJson, while removing duplicate payload-tree allocations from the synchronous Event Transport observer path. @@ -981,16 +991,16 @@ A project using only the core Serial library: ```ini lib_deps = - flowduino/ESPressio-Serial@^0.5.1 + flowduino/ESPressio-Serial@^0.5.2 ``` An application using Event Monitor requires: ```ini lib_deps = - flowduino/ESPressio-Serial@^0.5.1 - flowduino/ESPressio-Event@^5.8.1 - flowduino/ESPressio-Serializable@^0.10.1 + flowduino/ESPressio-Serial@^0.5.2 + flowduino/ESPressio-Event@^5.8.2 + flowduino/ESPressio-Serializable@^0.10.2 ``` The Event/Serializable dependencies are intentionally not declared as mandatory package dependencies of ESPressio Serial because they are required only by the opt-in Event Monitor feature. @@ -1004,16 +1014,16 @@ The generic console requires only ESPressio Serial: ```ini lib_deps = - flowduino/ESPressio-Serial@^0.5.1 + flowduino/ESPressio-Serial@^0.5.2 ``` The Event Console additionally requires the runtime Event and JSON stacks: ```ini lib_deps = - flowduino/ESPressio-Serial@^0.5.1 - flowduino/ESPressio-Event@^5.8.1 - flowduino/ESPressio-Serializable@^0.10.1 + flowduino/ESPressio-Serial@^0.5.2 + flowduino/ESPressio-Event@^5.8.2 + flowduino/ESPressio-Serializable@^0.10.2 bblanchon/ArduinoJson ``` diff --git a/component.mk b/component.mk index 02b1fdc..3492875 100644 --- a/component.mk +++ b/component.mk @@ -21,5 +21,5 @@ CPPFLAGS += \ -DESPRESSIO_SERIAL \ -DESPRESSIO_SERIAL_VERSION_MAJOR=0 \ -DESPRESSIO_SERIAL_VERSION_MINOR=5 \ - -DESPRESSIO_SERIAL_VERSION_PATCH=0 \ - -DESPRESSIO_SERIAL_VERSION_STRING=\"0.5.0\" + -DESPRESSIO_SERIAL_VERSION_PATCH=2 \ + -DESPRESSIO_SERIAL_VERSION_STRING=\"0.5.2\" diff --git a/library.json b/library.json index 67f6bb2..7223bd4 100644 --- a/library.json +++ b/library.json @@ -16,7 +16,7 @@ "type": "git", "url": "https://github.com/Flowduino/ESPressio-Serial.git" }, - "version": "0.5.1", + "version": "0.5.2", "license": "Apache-2.0", "frameworks": "arduino", "platforms": "espressif32" diff --git a/library.properties b/library.properties index 6c52610..27344cf 100644 --- a/library.properties +++ b/library.properties @@ -1,5 +1,5 @@ name=ESPressio-Serial -version=0.5.1 +version=0.5.2 author=Flowduino maintainer=Flowduino sentence=Serial console, diagnostics, logging and operator tooling for the ESPressio ecosystem. diff --git a/src/console/ESPressio_Console.hpp b/src/console/ESPressio_Console.hpp index ee4bf2d..eb41aa7 100644 --- a/src/console/ESPressio_Console.hpp +++ b/src/console/ESPressio_Console.hpp @@ -92,7 +92,6 @@ class Console final { static_cast( right[index] ) - ) ) { return false; } @@ -200,10 +199,22 @@ class Console final { Print& output, const ConsoleConfig& config = {} ) { + ConsoleConfig preparedConfig; + std::string preparedLine; + + try { + preparedConfig = config; + preparedLine.reserve( + preparedConfig.MaximumLineLength + ); + } catch (...) { + return false; + } + _input = &input; _output = &output; - _config = config; - _line.clear(); + _config = std::move(preparedConfig); + _line = std::move(preparedLine); _discardUntilNewline = false; PrintPrompt(); @@ -232,6 +243,12 @@ class Console final { return _output; } +#ifdef ESPRESSIO_SERIAL_TESTING + std::size_t __GetInputBufferCapacityForTesting() const noexcept { + return _line.capacity(); + } +#endif + uint32_t RegisterLineInterceptor( ConsoleLineInterceptor interceptor ) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 10eddfc..9857137 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -14,6 +14,7 @@ add_executable( target_compile_features(test_console PRIVATE cxx_std_17) target_compile_options(test_console PRIVATE -Wall -Wextra -Wpedantic -Werror) +target_compile_definitions(test_console PRIVATE ESPRESSIO_SERIAL_TESTING) target_include_directories(test_console PRIVATE stubs ../src) add_test(NAME Console COMMAND test_console) diff --git a/tests/test_console.cpp b/tests/test_console.cpp index 6820e30..46c2f95 100644 --- a/tests/test_console.cpp +++ b/tests/test_console.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -52,10 +53,31 @@ int main() { TestStream stream; + { + Console impossibleConsole; + ConsoleConfig impossibleConfig; + impossibleConfig.ShowPrompt = false; + impossibleConfig.MaximumLineLength = + std::numeric_limits::max(); + + assert( + !impossibleConsole.Initialize( + stream, + stream, + impossibleConfig + ) + ); + + assert( + !impossibleConsole.GetIsInitialized() + ); + } + Console console; ConsoleConfig config; config.ShowPrompt = false; + config.MaximumLineLength = 128; assert( console.Initialize( @@ -65,6 +87,14 @@ int main() { ) ); + const auto reservedCapacity = + console.__GetInputBufferCapacityForTesting(); + + assert( + reservedCapacity >= + config.MaximumLineLength + ); + bool called = false; std::string lastArguments; @@ -145,6 +175,48 @@ int main() { assert(called); assert(lastArguments == "from poll"); + assert( + console.__GetInputBufferCapacityForTesting() == + reservedCapacity + ); + + stream.Input = + std::string( + config.MaximumLineLength, + 'x' + ) + + "\n"; + + stream.ReadOffset = 0; + console.Poll(); + + assert( + console.__GetInputBufferCapacityForTesting() == + reservedCapacity + ); + + stream.Input = + std::string( + config.MaximumLineLength + 1, + 'y' + ) + + "\n"; + + stream.ReadOffset = 0; + stream.Output.clear(); + console.Poll(); + + assert( + stream.Output.find( + "Input rejected: line exceeds configured maximum length." + ) != + std::string::npos + ); + + assert( + console.__GetInputBufferCapacityForTesting() == + reservedCapacity + ); return 0; }