Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 43 additions & 11 deletions src/FSCommon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,13 @@ bool renameFile(const char *pathFrom, const char *pathTo)
#endif
}

#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <new>
#include <stdexcept>
#include <vector>
#ifdef ARCH_ESP32
#include <esp_heap_caps.h>
#endif

/**
* @brief Platform-agnostic filesystem format / wipe.
Expand Down Expand Up @@ -250,6 +253,12 @@ void collectFiles(const char *dirname, uint8_t levels, size_t maxCount, std::vec
} // namespace
#endif

#ifdef ARCH_ESP32
// Headroom kept below the allocator's largest free block when sizing the manifest: the block reported
// includes the allocator's own bookkeeping, and other tasks keep allocating while the SPI lock is held.
static constexpr size_t FILES_MANIFEST_HEAP_MARGIN = 1024;
#endif

/**
* @brief Get the list of files in a directory.
*
Expand All @@ -268,18 +277,41 @@ std::vector<meshtastic_FileInfo> getFiles(const char *dirname, uint8_t levels, s
if (wasLimited)
*wasLimited = false;
#ifdef FSCom
#if defined(__cpp_exceptions) || defined(__EXCEPTIONS)
size_t reservedCount = maxCount;
// Size the vector once, up front, to what the heap can actually hand out, and cap the walk at that
// count so push_back() never has to grow it. Any allocation that fails here goes through operator
// new and raises std::bad_alloc; the ESP32 framework is built with CONFIG_COMPILER_CXX_EXCEPTIONS=n,
// so there is no unwinder and a throw is std::terminate() -> abort() -> reboot. That fires on the
// very first client handshake whenever the heap is fragmented (WiFi + TLS up, no PSRAM), which is
// exactly when this runs. So: never let reserve() be the thing that discovers there is no room.
// Cap at what a vector of FileInfo can hold at all: it keeps the probe's byte count from wrapping
// for a huge maxCount, and it is also the bound reserve() would otherwise reject with a throw.
size_t reservedCount = std::min(maxCount, filenames.max_size());
#ifdef ARCH_ESP32
// Ask the allocator for the largest contiguous block malloc() could hand out. MALLOC_CAP_DEFAULT
// is the capability heap_caps_malloc_default() (what operator new resolves to) falls back to
// across every region, internal and PSRAM alike, so this is the "will new succeed" question
// asked directly. Nothing is freed before the reserve, so there is no hole for another task to
// take between the probe and the allocation.
const size_t largest = heap_caps_get_largest_free_block(MALLOC_CAP_DEFAULT);
// Leave a margin below the largest block: the allocator's own overhead sits inside it, and other
// threads keep allocating while we hold the SPI lock.
const size_t usable = largest > FILES_MANIFEST_HEAP_MARGIN ? largest - FILES_MANIFEST_HEAP_MARGIN : 0;
reservedCount = std::min(reservedCount, usable / sizeof(meshtastic_FileInfo));
#else
// Other targets have no largest-block query. Probe with malloc() - the allocation that returns
// nullptr on failure under every build (new(std::nothrow) is not that: libstdc++ implements it as
// a try/catch around the throwing form) - free the probe, and reserve the size that fit. Not
// airtight against a concurrent allocator, but the SPI lock the caller holds serialises the usual
// competitors and it is strictly better than letting reserve() be the first to find out.
while (reservedCount > 0) {
try {
filenames.reserve(reservedCount);
void *probe = malloc(reservedCount * sizeof(meshtastic_FileInfo));
if (probe) {
free(probe);
break;
} catch (const std::bad_alloc &) {
reservedCount /= 2;
} catch (const std::length_error &) {
reservedCount /= 2;
}
reservedCount /= 2;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
#endif
if (reservedCount == 0) {
if (wasLimited)
*wasLimited = true;
Expand All @@ -290,7 +322,7 @@ std::vector<meshtastic_FileInfo> getFiles(const char *dirname, uint8_t levels, s
*wasLimited = true;
maxCount = reservedCount;
}
#endif
filenames.reserve(reservedCount);
collectFiles(dirname, levels, maxCount, filenames, wasLimited);
#endif
return filenames;
Expand Down
6 changes: 3 additions & 3 deletions src/mesh/PhoneAPI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -325,10 +325,10 @@ void PhoneAPI::handleStartConfig()
filesManifest = getFiles("/", FILES_MANIFEST_LEVELS, FILES_MANIFEST_MAX_COUNT, &filesManifestLimited);
}
if (filesManifestLimited) {
LOG_WARN("Got %zu files in manifest (limited to %zu entries/depth %u)", filesManifest.size(),
FILES_MANIFEST_MAX_COUNT, static_cast<unsigned>(FILES_MANIFEST_LEVELS));
LOG_WARN("Got %u files in manifest (limited to %u entries/depth %u)", (unsigned)filesManifest.size(),
(unsigned)FILES_MANIFEST_MAX_COUNT, static_cast<unsigned>(FILES_MANIFEST_LEVELS));
} else {
LOG_DEBUG("Got %zu files in manifest", filesManifest.size());
LOG_DEBUG("Got %u files in manifest", (unsigned)filesManifest.size());
}
} else {
releaseFilesManifest(filesManifest);
Expand Down
19 changes: 18 additions & 1 deletion src/mesh/api/ServerAPI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
#include "ServerAPI.h"
#include "Throttle.h"
#include <Arduino.h>
#include <cstdlib>
#include <new>

static constexpr uint32_t TCP_IDLE_TIMEOUT_MS = 15 * 60 * 1000UL;

Expand Down Expand Up @@ -117,7 +119,22 @@ template <class T, class U> int32_t APIServerPort<T, U>::runOnce()
openAPI.reset();
}

openAPI.reset(new T(client));
// A ServerAPI carries the stream rx/tx buffers plus the FromRadio/ToRadio scratch, several
// KB in one block. On ESP32 a new that cannot get that block is a reboot (see the note on
// openAPI in the header), and std::nothrow does not help there because libstdc++ builds it
// on the throwing form. malloc() does return nullptr, so take the block from malloc() and
// construct in place; if there is no room drop this connection instead of the node - the
// client retries and the next accept gets a fresh look at the heap. The T constructors do
// not allocate (default-constructed containers, fixed-size thread table), so nothing inside
// the placement new can throw either.
void *block = malloc(sizeof(T));
if (!block) {
LOG_ERROR("No heap for API connection (%u bytes), dropping client", (unsigned)sizeof(T));
client.stop();
} else {
openAPI.reset(new (block) T(client));
}
// cppcheck-suppress memleak ; block is owned by openAPI via placement new, freed by MallocDeleter
}

#if RAK_4631
Expand Down
18 changes: 17 additions & 1 deletion src/mesh/api/ServerAPI.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include "StreamAPI.h"
#include <cstdlib>
#include <memory>

#define SERVER_API_DEFAULT_PORT 4403
Expand Down Expand Up @@ -44,8 +45,23 @@ template <class T, class U> class APIServerPort : public U, private concurrency:
*
* FIXME: We currently only allow one open TCP connection at a time, because we depend on the loop() call in this class to
* delegate to the worker. Once coroutines are implemented we can relax this restriction.
*
* The ServerAPI is built in a malloc()'d block with placement new rather than operator new: on ESP32 the framework
* is compiled with CONFIG_COMPILER_CXX_EXCEPTIONS=n and every throw is wrapped to abort(), which makes a failed
* operator new - the plain form and, because libstdc++ implements it as a try/catch around the plain form, the
* std::nothrow form too - a reboot. malloc() is the one allocation on that platform that hands back nullptr, so
* a fragmented heap drops the incoming client instead of the node. The deleter runs the destructor and free()s.
*/
std::unique_ptr<T> openAPI;
struct MallocDeleter {
void operator()(T *p) const
{
if (p) {
p->~T();
free(p);
}
}
};
std::unique_ptr<T, MallocDeleter> openAPI;
#if defined(RAK_4631) || defined(RAK11310)
// Track wait time for RAK13800 Ethernet requests
int32_t waitTime = 100;
Expand Down
Loading