From 13643fdb45f5c2de22d0a8cd5dbb86a02fff4451 Mon Sep 17 00:00:00 2001 From: Mitch Bradley Date: Thu, 2 Apr 2026 12:45:02 -1000 Subject: [PATCH 1/5] emptyString -> (locally defined) _emptyString because some Arduino cores do not have emptyString and adding it is cumbersome. --- src/WebAuthentication.cpp | 22 +++++++++++----------- src/WebHandlers.cpp | 2 +- src/WebRequest.cpp | 32 ++++++++++++++++---------------- src/WebResponseImpl.h | 2 +- src/WebServer.cpp | 2 ++ 5 files changed, 31 insertions(+), 29 deletions(-) diff --git a/src/WebAuthentication.cpp b/src/WebAuthentication.cpp index 64509e07b..3e27335db 100644 --- a/src/WebAuthentication.cpp +++ b/src/WebAuthentication.cpp @@ -26,19 +26,19 @@ bool checkBasicAuthentication(const char *hash, const char *username, const char String generateBasicHash(const char *username, const char *password) { if (username == NULL || password == NULL) { - return emptyString; + return _emptyString; } size_t toencodeLen = strlen(username) + strlen(password) + 1; char *toencode = new char[toencodeLen + 1]; if (toencode == NULL) { - return emptyString; + return _emptyString; } char *encoded = new char[base64_encode_expected_len(toencodeLen) + 1]; if (encoded == NULL) { delete[] toencode; - return emptyString; + return _emptyString; } sprintf_P(toencode, PSTR("%s:%s"), username, password); if (base64_encode_chars(toencode, toencodeLen, encoded) > 0) { @@ -49,11 +49,11 @@ String generateBasicHash(const char *username, const char *password) { } delete[] toencode; delete[] encoded; - return emptyString; + return _emptyString; } static bool getMD5(uint8_t *data, uint16_t len, char *output) { // 33 bytes or more -#if defined(ESP32) || defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) +#if defined(ESP32) || defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(HOST) MD5Builder md5; md5.begin(); md5.add(data, len); @@ -90,7 +90,7 @@ String genRandomMD5() { char *out = (char *)malloc(33); if (out == NULL || !getMD5((uint8_t *)(&r), 4, out)) { async_ws_log_e("Failed to allocate"); - return emptyString; + return _emptyString; } String res = String(out); free(out); @@ -101,7 +101,7 @@ static String stringMD5(const String &in) { char *out = (char *)malloc(33); if (out == NULL || !getMD5((uint8_t *)(in.c_str()), in.length(), out)) { async_ws_log_e("Failed to allocate"); - return emptyString; + return _emptyString; } String res = String(out); free(out); @@ -110,19 +110,19 @@ static String stringMD5(const String &in) { String generateDigestHash(const char *username, const char *password, const char *realm) { if (username == NULL || password == NULL || realm == NULL) { - return emptyString; + return _emptyString; } char *out = (char *)malloc(33); if (out == NULL) { async_ws_log_e("Failed to allocate"); - return emptyString; + return _emptyString; } String in; if (!in.reserve(strlen(username) + strlen(realm) + strlen(password) + 2)) { async_ws_log_e("Failed to allocate"); free(out); - return emptyString; + return _emptyString; } in.concat(username); @@ -134,7 +134,7 @@ String generateDigestHash(const char *username, const char *password, const char if (!getMD5((uint8_t *)(in.c_str()), in.length(), out)) { async_ws_log_e("Failed to allocate"); free(out); - return emptyString; + return _emptyString; } in = String(out); diff --git a/src/WebHandlers.cpp b/src/WebHandlers.cpp index 698ab0064..f67e8c384 100644 --- a/src/WebHandlers.cpp +++ b/src/WebHandlers.cpp @@ -260,7 +260,7 @@ void AsyncStaticWebHandler::handleRequest(AsyncWebServerRequest *request) { request->_tempFile.close(); response = new AsyncBasicResponse(304); // Not modified } else { - response = new AsyncFileResponse(request->_tempFile, filename, emptyString, false, _callback); + response = new AsyncFileResponse(request->_tempFile, filename, _emptyString, false, _callback); } if (!response) { diff --git a/src/WebRequest.cpp b/src/WebRequest.cpp index 87593bbdd..6f749f953 100644 --- a/src/WebRequest.cpp +++ b/src/WebRequest.cpp @@ -298,7 +298,7 @@ void AsyncWebServerRequest::_addGetParams(const String ¶ms) { equal = end; } String name = urlDecode(params.substring(start, equal)); - String value = urlDecode(equal + 1 < end ? params.substring(equal + 1, end) : emptyString); + String value = urlDecode(equal + 1 < end ? params.substring(equal + 1, end) : _emptyString); if (name.length()) { _params.emplace_back(name, value); } @@ -336,7 +336,7 @@ bool AsyncWebServerRequest::_parseReqHead() { _version = 1; } - _temp = emptyString; + _temp = _emptyString; return true; } @@ -545,7 +545,7 @@ bool AsyncWebServerRequest::_parseReqHeader() { } #if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(LIBRETINY) // Ancient PRI core does not have String::clear() method 8-() - _temp = emptyString; + _temp = _emptyString; #else _temp.clear(); #endif @@ -568,9 +568,9 @@ void AsyncWebServerRequest::_parsePlainPostChar(uint8_t data) { _params.emplace_back(name, urlDecode(value), true); } -#if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(LIBRETINY) +#if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(LIBRETINY) || defined(HOST) // Ancient PRI core does not have String::clear() method 8-() - _temp = emptyString; + _temp = _emptyString; #else _temp.clear(); #endif @@ -615,10 +615,10 @@ void AsyncWebServerRequest::_parseMultipartPostByte(uint8_t data, bool last) { if (!_parsedLength) { _multiParseState = EXPECT_BOUNDARY; - _temp = emptyString; - _itemName = emptyString; - _itemFilename = emptyString; - _itemType = emptyString; + _temp = _emptyString; + _itemName = _emptyString; + _itemFilename = _emptyString; + _itemType = _emptyString; } if (_multiParseState == WAIT_FOR_RETURN1) { @@ -686,13 +686,13 @@ void AsyncWebServerRequest::_parseMultipartPostByte(uint8_t data, bool last) { _params.emplace_back(T_filename, _itemFilename, true, true); } } - _temp = emptyString; + _temp = _emptyString; } else { _multiParseState = WAIT_FOR_RETURN1; // value starts from here _itemSize = 0; _itemStartIndex = _parsedLength; - _itemValue = emptyString; + _itemValue = _emptyString; if (_itemIsFile) { if (_itemBuffer) { free(_itemBuffer); @@ -1219,7 +1219,7 @@ const String &AsyncWebServerRequest::arg(const char *name) const { return arg.value(); } } - return emptyString; + return _emptyString; } #ifdef ESP8266 @@ -1238,7 +1238,7 @@ const String &AsyncWebServerRequest::argName(size_t i) const { const String &AsyncWebServerRequest::header(const char *name) const { const AsyncWebHeader *h = getHeader(name); - return h ? h->value() : emptyString; + return h ? h->value() : _emptyString; } #ifdef ESP8266 @@ -1249,12 +1249,12 @@ const String &AsyncWebServerRequest::header(const __FlashStringHelper *data) con const String &AsyncWebServerRequest::header(size_t i) const { const AsyncWebHeader *h = getHeader(i); - return h ? h->value() : emptyString; + return h ? h->value() : _emptyString; } const String &AsyncWebServerRequest::headerName(size_t i) const { const AsyncWebHeader *h = getHeader(i); - return h ? h->name() : emptyString; + return h ? h->name() : _emptyString; } String AsyncWebServerRequest::urlDecode(const String &text) const { @@ -1265,7 +1265,7 @@ String AsyncWebServerRequest::urlDecode(const String &text) const { // Allocate the string internal buffer - never longer from source text if (!decoded.reserve(len)) { async_ws_log_e("Failed to allocate"); - return emptyString; + return _emptyString; } while (i < len) { char decodedChar; diff --git a/src/WebResponseImpl.h b/src/WebResponseImpl.h index 747d4a2b1..d093c6177 100644 --- a/src/WebResponseImpl.h +++ b/src/WebResponseImpl.h @@ -37,7 +37,7 @@ class AsyncBasicResponse : public AsyncWebServerResponse { public: explicit AsyncBasicResponse(int code, const char *contentType = asyncsrv::empty, const char *content = asyncsrv::empty); - AsyncBasicResponse(int code, const String &contentType, const String &content = emptyString) + AsyncBasicResponse(int code, const String &contentType, const String &content = _emptyString) : AsyncBasicResponse(code, contentType.c_str(), content.c_str()) {} void _respond(AsyncWebServerRequest *request) final; size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time) final { diff --git a/src/WebServer.cpp b/src/WebServer.cpp index 15fa701af..ae0da264c 100644 --- a/src/WebServer.cpp +++ b/src/WebServer.cpp @@ -343,3 +343,5 @@ bool AsyncURIMatcher::matches(AsyncWebServerRequest *request) const { return false; } } + +const String _emptyString(""); From fd98dd179654f3e1f5b654b8d877fa13d8667f4b Mon Sep 17 00:00:00 2001 From: Mitch Bradley Date: Thu, 2 Apr 2026 12:48:37 -1000 Subject: [PATCH 2/5] ifdefs for hosted build, which needs locking like ESP32 does --- src/AsyncEventSource.cpp | 10 +++++++--- src/AsyncEventSource.h | 4 ++-- src/AsyncWebServerLogging.h | 7 +++++++ src/AsyncWebSocket.cpp | 31 ++++++++++++++++++++----------- src/AsyncWebSocket.h | 19 +++++++++++++++++-- src/ESPAsyncWebServer.h | 32 ++++++++++++++++++++++---------- src/WebAuthentication.cpp | 4 +++- src/WebRequest.cpp | 3 ++- src/WebResponseImpl.h | 4 ++++ src/WebResponses.cpp | 33 +++++++++++++++++++++++++++++++++ src/WebServer.cpp | 2 +- 11 files changed, 118 insertions(+), 31 deletions(-) diff --git a/src/AsyncEventSource.cpp b/src/AsyncEventSource.cpp index 60fd92735..7a9ee04f2 100644 --- a/src/AsyncEventSource.cpp +++ b/src/AsyncEventSource.cpp @@ -27,7 +27,7 @@ static String generateEventMessage(const char *message, const char *event, uint3 if (!str.reserve(len)) { async_ws_log_e("Failed to allocate"); - return emptyString; + return _emptyString; } if (reconnect) { @@ -191,7 +191,7 @@ AsyncEventSourceClient::AsyncEventSourceClient(AsyncWebServerRequest *request, A } AsyncEventSourceClient::~AsyncEventSourceClient() { -#ifdef ESP32 +#if defined(ESP32) || defined(HOST) // Protect message queue access (size checks and modifications) which is not thread-safe. std::lock_guard lock(_lockmq); #endif @@ -200,8 +200,12 @@ AsyncEventSourceClient::~AsyncEventSourceClient() { } bool AsyncEventSourceClient::_queueMessage(const char *message, size_t len) { -#ifdef ESP32 +#if defined(ESP32) || defined(HOST) // Protect message queue access (size checks and modifications) which is not thread-safe. + if (_messageQueue.size() >= SSE_MAX_QUEUED_MESSAGES) { + async_ws_log_e("Event message queue overflow: discard message"); + return false; + } std::lock_guard lock(_lockmq); #endif diff --git a/src/AsyncEventSource.h b/src/AsyncEventSource.h index 4282fff3b..b1d68433e 100644 --- a/src/AsyncEventSource.h +++ b/src/AsyncEventSource.h @@ -5,7 +5,7 @@ #include -#if defined(ESP32) || defined(LIBRETINY) +#if defined(ESP32) || defined(LIBRETINY) || defined(HOST) #include #ifdef LIBRETINY #ifdef round @@ -136,7 +136,7 @@ class AsyncEventSourceClient { size_t _inflight{0}; // num of unacknowledged bytes that has been written to socket buffer size_t _max_inflight{SSE_MAX_INFLIGH}; // max num of unacknowledged bytes that could be written to socket buffer std::list _messageQueue; -#ifdef ESP32 +#if defined(ESP32) || defined(HOST) mutable std::recursive_mutex _lockmq; #endif bool _queueMessage(const char *message, size_t len); diff --git a/src/AsyncWebServerLogging.h b/src/AsyncWebServerLogging.h index f550fd9ec..daef25942 100644 --- a/src/AsyncWebServerLogging.h +++ b/src/AsyncWebServerLogging.h @@ -31,6 +31,13 @@ #define async_ws_log_d(format, ...) log_d(format, ##__VA_ARGS__) #define async_ws_log_v(format, ...) log_v(format, ##__VA_ARGS__) +#elif defined(HOST) +#define async_ws_log_e(format, ...) +#define async_ws_log_w(format, ...) +#define async_ws_log_i(format, ...) +#define async_ws_log_d(format, ...) +#define async_ws_log_v(format, ...) + /** * Raspberry Pi Pico specific configurations */ diff --git a/src/AsyncWebSocket.cpp b/src/AsyncWebSocket.cpp index 1f6adee01..01af0bf56 100644 --- a/src/AsyncWebSocket.cpp +++ b/src/AsyncWebSocket.cpp @@ -17,6 +17,8 @@ #include #elif defined(LIBRETINY) #include +#elif defined(HOST) +#include "BackPort_SHA1Builder.h" #endif #include @@ -261,7 +263,14 @@ AsyncWebSocketClient::AsyncWebSocketClient(AsyncClient *client, AsyncWebSocket * _client->onDisconnect( [](void *r, AsyncClient *c) { ((AsyncWebSocketClient *)(r))->_onDisconnect(); +#if defined(HOST) + // Do NOT delete c here: in the host build, AsyncClient lifetime is managed by + // std::shared_ptr in AsyncTCPManager::_clients. The manager removes the + // shared_ptr on the next poll cycle once it sees _socket < 0 (set by _close()). + (void)c; +#else delete c; +#endif }, this ); @@ -291,7 +300,7 @@ AsyncWebSocketClient::AsyncWebSocketClient(AsyncClient *client, AsyncWebSocket * AsyncWebSocketClient::~AsyncWebSocketClient() { { -#ifdef ESP32 +#if defined(ESP32) || defined(HOST) std::lock_guard lock(_lock); #endif _messageQueue.clear(); @@ -309,7 +318,7 @@ void AsyncWebSocketClient::_clearQueue() { void AsyncWebSocketClient::_onAck(size_t len, uint32_t time) { _lastMessageTime = millis(); -#ifdef ESP32 +#if defined(ESP32) || defined(HOST) std::unique_lock lock(_lock); #endif @@ -324,7 +333,7 @@ void AsyncWebSocketClient::_onAck(size_t len, uint32_t time) { _status = WS_DISCONNECTED; async_ws_log_v("[%s][%" PRIu32 "] ACK WS_DISCONNECTED", _server->url(), _clientId); if (_client) { -#ifdef ESP32 +#if defined(ESP32) || defined(HOST) /* Unlocking has to be called before return execution otherwise std::unique_lock ::~unique_lock() will get an exception pthread_mutex_unlock. Due to _client->close() shall call the callback function _onDisconnect() @@ -361,13 +370,13 @@ void AsyncWebSocketClient::_onPoll() { return; } -#ifdef ESP32 +#if defined(ESP32) || defined(HOST) std::unique_lock lock(_lock); #endif if (_client && _client->canSend() && (!_controlQueue.empty() || !_messageQueue.empty())) { _runQueue(); } else if (_keepAlivePeriod > 0 && (millis() - _lastMessageTime) >= _keepAlivePeriod && (_controlQueue.empty() && _messageQueue.empty())) { -#ifdef ESP32 +#if defined(ESP32) || defined(HOST) lock.unlock(); #endif ping((uint8_t *)AWSC_PING_PAYLOAD, AWSC_PING_PAYLOAD_LEN); @@ -433,21 +442,21 @@ void AsyncWebSocketClient::_runQueue() { } bool AsyncWebSocketClient::queueIsFull() const { -#ifdef ESP32 +#if defined(ESP32) || defined(HOST) std::lock_guard lock(_lock); #endif return (_messageQueue.size() >= WS_MAX_QUEUED_MESSAGES) || (_status != WS_CONNECTED); } size_t AsyncWebSocketClient::queueLen() const { -#ifdef ESP32 +#if defined(ESP32) || defined(HOST) std::lock_guard lock(_lock); #endif return _messageQueue.size(); } bool AsyncWebSocketClient::canSend() const { -#ifdef ESP32 +#if defined(ESP32) || defined(HOST) std::lock_guard lock(_lock); #endif return _messageQueue.size() < WS_MAX_QUEUED_MESSAGES; @@ -458,7 +467,7 @@ bool AsyncWebSocketClient::_queueControl(uint8_t opcode, const uint8_t *data, si return false; } -#ifdef ESP32 +#if defined(ESP32) || defined(HOST) std::lock_guard lock(_lock); #endif @@ -477,7 +486,7 @@ bool AsyncWebSocketClient::_queueMessage(AsyncWebSocketSharedBuffer buffer, uint return false; } -#ifdef ESP32 +#if defined(ESP32) || defined(HOST) std::unique_lock lock(_lock); #endif @@ -486,7 +495,7 @@ bool AsyncWebSocketClient::_queueMessage(AsyncWebSocketSharedBuffer buffer, uint _status = WS_DISCONNECTED; if (_client) { -#ifdef ESP32 +#if defined(ESP32) || defined(HOST) /* Unlocking has to be called before return execution otherwise std::unique_lock ::~unique_lock() will get an exception pthread_mutex_unlock. Due to _client->close() shall call the callback function _onDisconnect() diff --git a/src/AsyncWebSocket.h b/src/AsyncWebSocket.h index ea181c590..99a7456e6 100644 --- a/src/AsyncWebSocket.h +++ b/src/AsyncWebSocket.h @@ -16,6 +16,16 @@ #ifndef WS_MAX_QUEUED_MESSAGES #define WS_MAX_QUEUED_MESSAGES 32 #endif +#elif defined(HOST) +// For compatibility with AsyncWebSocket +#ifndef FPSTR +#define FPSTR (const char *) +#endif + +#include +#ifndef WS_MAX_QUEUED_MESSAGES +#define WS_MAX_QUEUED_MESSAGES 32 +#endif #elif defined(ESP8266) #include #ifndef WS_MAX_QUEUED_MESSAGES @@ -44,7 +54,7 @@ #endif #ifndef DEFAULT_MAX_WS_CLIENTS -#ifdef ESP32 +#if defined(ESP32) || defined(HOST) #define DEFAULT_MAX_WS_CLIENTS 8 #else #define DEFAULT_MAX_WS_CLIENTS 4 @@ -222,7 +232,7 @@ class AsyncWebSocketClient { uint8_t _pstate; uint32_t _lastMessageTime; uint32_t _keepAlivePeriod; -#ifdef ESP32 +#if defined(ESP32) || defined(HOST) mutable std::recursive_mutex _lock; #endif std::deque _controlQueue; @@ -372,8 +382,13 @@ class AsyncWebSocket : public AsyncWebHandler { AwsEventHandler _eventHandler; AwsHandshakeHandler _handshakeHandler; bool _enabled; +<<<<<<< HEAD #ifdef ESP32 mutable std::recursive_mutex _lock; +======= +#if defined(ESP32) || defined(HOST) + mutable std::mutex _lock; +>>>>>>> 24f7507 (Support hosted builds using Arduino-Emulator) #endif public: diff --git a/src/ESPAsyncWebServer.h b/src/ESPAsyncWebServer.h index f7e222d1b..19544df96 100644 --- a/src/ESPAsyncWebServer.h +++ b/src/ESPAsyncWebServer.h @@ -5,7 +5,9 @@ #include #include +#ifndef HOST #include +#endif #include #include @@ -37,6 +39,9 @@ #if defined(ESP32) || defined(LIBRETINY) #include #include +#elif defined(HOST) +#include +#include #elif defined(ESP8266) #include #elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) @@ -374,6 +379,13 @@ typedef std::function AwsTemplateProcessor; using AsyncWebServerRequestPtr = std::weak_ptr; +// The ESP32 Arduino framework defines emptyString but it is not in the +// Arduino Core API. Some other Arduino frameworks define emptyString +// for compatibility, but its implementation is cumbersome due to the +// lack of a convenient place to put the definition. For maximum +// portablity, we just make our own renamed version. +extern const String _emptyString; + class AsyncWebServerRequest { using File = fs::File; using FS = fs::FS; @@ -671,14 +683,14 @@ class AsyncWebServerRequest { AsyncWebServerResponse * beginResponse(FS &fs, const String &path, const char *contentType = asyncsrv::empty, bool download = false, AwsTemplateProcessor callback = nullptr); AsyncWebServerResponse * - beginResponse(FS &fs, const String &path, const String &contentType = emptyString, bool download = false, AwsTemplateProcessor callback = nullptr) { + beginResponse(FS &fs, const String &path, const String &contentType = _emptyString, bool download = false, AwsTemplateProcessor callback = nullptr) { return beginResponse(fs, path, contentType.c_str(), download, callback); } AsyncWebServerResponse * beginResponse(File content, const String &path, const char *contentType = asyncsrv::empty, bool download = false, AwsTemplateProcessor callback = nullptr); AsyncWebServerResponse * - beginResponse(File content, const String &path, const String &contentType = emptyString, bool download = false, AwsTemplateProcessor callback = nullptr) { + beginResponse(File content, const String &path, const String &contentType = _emptyString, bool download = false, AwsTemplateProcessor callback = nullptr) { return beginResponse(content, path, contentType.c_str(), download, callback); } @@ -783,11 +795,11 @@ class AsyncWebServerRequest { #endif const String &arg(size_t i) const; // get request argument value by number const String &arg(int i) const { - return i < 0 ? emptyString : arg((size_t)i); + return i < 0 ? _emptyString : arg((size_t)i); }; const String &argName(size_t i) const; // get request argument name by number const String &argName(int i) const { - return i < 0 ? emptyString : argName((size_t)i); + return i < 0 ? _emptyString : argName((size_t)i); }; bool hasArg(const char *name) const; // check if argument exists bool hasArg(const String &name) const { @@ -800,14 +812,14 @@ class AsyncWebServerRequest { #ifdef ASYNCWEBSERVER_REGEX const String &pathArg(size_t i) const { if (i >= _pathParams.size()) { - return emptyString; + return _emptyString; } auto it = _pathParams.begin(); std::advance(it, i); return *it; } const String &pathArg(int i) const { - return i < 0 ? emptyString : pathArg((size_t)i); + return i < 0 ? _emptyString : pathArg((size_t)i); } #else const String &pathArg(size_t i) const __attribute__((error("ERR: pathArg() requires -D ASYNCWEBSERVER_REGEX and only works on regex handlers"))); @@ -826,11 +838,11 @@ class AsyncWebServerRequest { const String &header(size_t i) const; // get request header value by number const String &header(int i) const { - return i < 0 ? emptyString : header((size_t)i); + return i < 0 ? _emptyString : header((size_t)i); }; const String &headerName(size_t i) const; // get request header name by number const String &headerName(int i) const { - return i < 0 ? emptyString : headerName((size_t)i); + return i < 0 ? _emptyString : headerName((size_t)i); }; size_t headers() const; // get header count @@ -888,7 +900,7 @@ class AsyncWebServerRequest { _attributes[name] = value; } void setAttribute(const char *name, bool value) { - _attributes[name] = value ? "1" : emptyString; + _attributes[name] = value ? "1" : _emptyString; } void setAttribute(const char *name, long value) { _attributes[name] = String(value); @@ -904,7 +916,7 @@ class AsyncWebServerRequest { return _attributes.find(name) != _attributes.end(); } - const String &getAttribute(const char *name, const String &defaultValue = emptyString) const; + const String &getAttribute(const char *name, const String &defaultValue = _emptyString) const; bool getAttribute(const char *name, bool defaultValue) const; long getAttribute(const char *name, long defaultValue) const; float getAttribute(const char *name, float defaultValue) const; diff --git a/src/WebAuthentication.cpp b/src/WebAuthentication.cpp index 3e27335db..d029f50d8 100644 --- a/src/WebAuthentication.cpp +++ b/src/WebAuthentication.cpp @@ -3,9 +3,11 @@ #include "WebAuthentication.h" #include "AsyncWebServerLogging.h" +#include "ESPAsyncWebServer.h" #include -#if defined(ESP32) || defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) + +#if defined(ESP32) || defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(HOST) #include #else #include diff --git a/src/WebRequest.cpp b/src/WebRequest.cpp index 6f749f953..e148d7aed 100644 --- a/src/WebRequest.cpp +++ b/src/WebRequest.cpp @@ -543,7 +543,8 @@ bool AsyncWebServerRequest::_parseReqHeader() { } _headers.emplace_back(std::move(header)); } -#if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(LIBRETINY) +#if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(LIBRETINY) || defined(HOST) + // Ancient PRI core does not have String::clear() method 8-() _temp = _emptyString; #else diff --git a/src/WebResponseImpl.h b/src/WebResponseImpl.h index d093c6177..32525d567 100644 --- a/src/WebResponseImpl.h +++ b/src/WebResponseImpl.h @@ -216,6 +216,10 @@ class AsyncResponseStream : public AsyncAbstractResponse, public Print { return (_state < RESPONSE_END); } size_t _fillBuffer(uint8_t *buf, size_t maxLen) final; +#ifdef ARDUINO_API_VERSION +// ArduinoCore-API does not have Print::printf + size_t printf(const char *format, ...); +#endif size_t write(const uint8_t *data, size_t len); size_t write(uint8_t data); /** diff --git a/src/WebResponses.cpp b/src/WebResponses.cpp index e520ea100..8f7f40722 100644 --- a/src/WebResponses.cpp +++ b/src/WebResponses.cpp @@ -923,6 +923,39 @@ size_t AsyncResponseStream::_fillBuffer(uint8_t *buf, size_t maxLen) { return _content->read((char *)buf, maxLen); } +#ifdef ARDUINO_API_VERSION +// ArduinoCore-API does not have Print::printf +#include +size_t AsyncResponseStream::printf(const char *format, ...) { + char loc_buf[64]; + char *temp = loc_buf; + va_list arg; + va_list copy; + va_start(arg, format); + va_copy(copy, arg); + int len = vsnprintf(temp, sizeof(loc_buf), format, copy); + va_end(copy); + if (len < 0) { + va_end(arg); + return 0; + } + if (len >= (int)sizeof(loc_buf)) { // comparation of same sign type for the compiler + temp = (char *)malloc(len + 1); + if (temp == NULL) { + va_end(arg); + return 0; + } + len = vsnprintf(temp, len + 1, format, arg); + } + va_end(arg); + len = write((uint8_t *)temp, len); + if (temp != loc_buf) { + free(temp); + } + return len; +} +#endif + size_t AsyncResponseStream::write(const uint8_t *data, size_t len) { if (_started()) { return 0; diff --git a/src/WebServer.cpp b/src/WebServer.cpp index ae0da264c..4108f1305 100644 --- a/src/WebServer.cpp +++ b/src/WebServer.cpp @@ -7,7 +7,7 @@ #include #include -#if defined(ESP32) || defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(LIBRETINY) +#if defined(ESP32) || defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(LIBRETINY) || defined(HOST) #include #elif defined(ESP8266) #include From e8297d7434680c4fd8e5a2b8267a8dbd4f63d92a Mon Sep 17 00:00:00 2001 From: Mitch Bradley Date: Thu, 2 Apr 2026 12:57:48 -1000 Subject: [PATCH 3/5] Added if defined(HOST) where needed --- src/AsyncEventSource.cpp | 18 +++++++++--------- src/AsyncEventSource.h | 4 ++-- src/AsyncWebSocket.cpp | 22 +++++++++++----------- src/AsyncWebSocket.h | 7 +------ 4 files changed, 23 insertions(+), 28 deletions(-) diff --git a/src/AsyncEventSource.cpp b/src/AsyncEventSource.cpp index 7a9ee04f2..3e740a31e 100644 --- a/src/AsyncEventSource.cpp +++ b/src/AsyncEventSource.cpp @@ -235,7 +235,7 @@ bool AsyncEventSourceClient::_queueMessage(const char *message, size_t len) { } bool AsyncEventSourceClient::_queueMessage(AsyncEvent_SharedData_t &&msg) { -#ifdef ESP32 +#if defined(ESP32) || defined(HOST) // Protect message queue access (size checks and modifications) which is not thread-safe. std::lock_guard lock(_lockmq); #endif @@ -265,7 +265,7 @@ bool AsyncEventSourceClient::_queueMessage(AsyncEvent_SharedData_t &&msg) { } void AsyncEventSourceClient::_onAck(size_t len __attribute__((unused)), uint32_t time __attribute__((unused))) { -#ifdef ESP32 +#if defined(ESP32) || defined(HOST) // Protect message queue access (size checks and modifications) which is not thread-safe. std::lock_guard lock(_lockmq); #endif @@ -293,7 +293,7 @@ void AsyncEventSourceClient::_onAck(size_t len __attribute__((unused)), uint32_t } void AsyncEventSourceClient::_onPoll() { -#ifdef ESP32 +#if defined(ESP32) || defined(HOST) // Protect message queue access (size checks and modifications) which is not thread-safe. std::lock_guard lock(_lockmq); #endif @@ -377,7 +377,7 @@ void AsyncEventSource::_addClient(AsyncEventSourceClient *client) { _connectcb(client); } -#ifdef ESP32 +#if defined(ESP32) || defined(HOST) std::lock_guard lock(_client_queue_lock); #endif @@ -390,7 +390,7 @@ void AsyncEventSource::_handleDisconnect(AsyncEventSourceClient *client) { if (_disconnectcb) { _disconnectcb(client); } -#ifdef ESP32 +#if defined(ESP32) || defined(HOST) std::lock_guard lock(_client_queue_lock); #endif for (auto i = _clients.begin(); i != _clients.end(); ++i) { @@ -406,7 +406,7 @@ void AsyncEventSource::close() { // While the whole loop is not done, the linked list is locked and so the // iterator should remain valid even when AsyncEventSource::_handleDisconnect() // is called very early -#ifdef ESP32 +#if defined(ESP32) || defined(HOST) std::lock_guard lock(_client_queue_lock); #endif for (const auto &c : _clients) { @@ -425,7 +425,7 @@ void AsyncEventSource::close() { size_t AsyncEventSource::avgPacketsWaiting() const { size_t aql = 0; uint32_t nConnectedClients = 0; -#ifdef ESP32 +#if defined(ESP32) || defined(HOST) std::lock_guard lock(_client_queue_lock); #endif for (const auto &c : _clients) { @@ -439,7 +439,7 @@ size_t AsyncEventSource::avgPacketsWaiting() const { AsyncEventSource::SendStatus AsyncEventSource::send(const char *message, const char *event, uint32_t id, uint32_t reconnect) { AsyncEvent_SharedData_t shared_msg = std::make_shared(generateEventMessage(message, event, id, reconnect)); -#ifdef ESP32 +#if defined(ESP32) || defined(HOST) std::lock_guard lock(_client_queue_lock); #endif size_t hits = 0; @@ -457,7 +457,7 @@ AsyncEventSource::SendStatus AsyncEventSource::send(const char *message, const c } size_t AsyncEventSource::count() const { -#ifdef ESP32 +#if defined(ESP32) || defined(HOST) std::lock_guard lock(_client_queue_lock); #endif size_t n_clients{0}; diff --git a/src/AsyncEventSource.h b/src/AsyncEventSource.h index b1d68433e..2dd869d36 100644 --- a/src/AsyncEventSource.h +++ b/src/AsyncEventSource.h @@ -205,7 +205,7 @@ class AsyncEventSourceClient { return _lastId; } size_t packetsWaiting() const { -#ifdef ESP32 +#if defined(ESP32) || defined(HOST) std::lock_guard lock(_lockmq); #endif return _messageQueue.size(); @@ -245,7 +245,7 @@ class AsyncEventSource : public AsyncWebHandler { private: String _url; std::list> _clients; -#ifdef ESP32 +#if defined(ESP32) || defined(HOST) // Same as for individual messages, protect mutations of _clients list // since simultaneous access from different tasks is possible mutable std::recursive_mutex _client_queue_lock; diff --git a/src/AsyncWebSocket.cpp b/src/AsyncWebSocket.cpp index 01af0bf56..52331150e 100644 --- a/src/AsyncWebSocket.cpp +++ b/src/AsyncWebSocket.cpp @@ -997,7 +997,7 @@ void AsyncWebSocket::_handleEvent(AsyncWebSocketClient *client, AwsEventType typ } AsyncWebSocketClient *AsyncWebSocket::_newClient(AsyncWebServerRequest *request) { -#ifdef ESP32 +#if defined(ESP32) || defined(HOST) std::lock_guard lock(_lock); #endif _clients.emplace_back(request, this); @@ -1009,7 +1009,7 @@ AsyncWebSocketClient *AsyncWebSocket::_newClient(AsyncWebServerRequest *request) } void AsyncWebSocket::_handleDisconnect(AsyncWebSocketClient *client) { -#ifdef ESP32 +#if defined(ESP32) || defined(HOST) std::lock_guard lock(_lock); #endif const auto client_id = client->id(); @@ -1022,7 +1022,7 @@ void AsyncWebSocket::_handleDisconnect(AsyncWebSocketClient *client) { } bool AsyncWebSocket::availableForWriteAll() { -#ifdef ESP32 +#if defined(ESP32) || defined(HOST) std::lock_guard lock(_lock); #endif return std::none_of(std::begin(_clients), std::end(_clients), [](const AsyncWebSocketClient &c) { @@ -1031,7 +1031,7 @@ bool AsyncWebSocket::availableForWriteAll() { } bool AsyncWebSocket::availableForWrite(uint32_t id) { -#ifdef ESP32 +#if defined(ESP32) || defined(HOST) std::lock_guard lock(_lock); #endif const auto iter = std::find_if(std::begin(_clients), std::end(_clients), [id](const AsyncWebSocketClient &c) { @@ -1044,7 +1044,7 @@ bool AsyncWebSocket::availableForWrite(uint32_t id) { } size_t AsyncWebSocket::count() const { -#ifdef ESP32 +#if defined(ESP32) || defined(HOST) std::lock_guard lock(_lock); #endif return std::count_if(std::begin(_clients), std::end(_clients), [](const AsyncWebSocketClient &c) { @@ -1053,7 +1053,7 @@ size_t AsyncWebSocket::count() const { } AsyncWebSocketClient *AsyncWebSocket::client(uint32_t id) { -#ifdef ESP32 +#if defined(ESP32) || defined(HOST) std::lock_guard lock(_lock); #endif const auto iter = std::find_if(_clients.begin(), _clients.end(), [id](const AsyncWebSocketClient &c) { @@ -1073,7 +1073,7 @@ void AsyncWebSocket::close(uint32_t id, uint16_t code, const char *message) { } void AsyncWebSocket::closeAll(uint16_t code, const char *message) { -#ifdef ESP32 +#if defined(ESP32) || defined(HOST) std::lock_guard lock(_lock); #endif for (auto &c : _clients) { @@ -1084,7 +1084,7 @@ void AsyncWebSocket::closeAll(uint16_t code, const char *message) { } void AsyncWebSocket::cleanupClients(uint16_t maxClients) { -#ifdef ESP32 +#if defined(ESP32) || defined(HOST) std::lock_guard lock(_lock); #endif const size_t c = count(); @@ -1107,7 +1107,7 @@ bool AsyncWebSocket::ping(uint32_t id, const uint8_t *data, size_t len) { } AsyncWebSocket::SendStatus AsyncWebSocket::pingAll(const uint8_t *data, size_t len) { -#ifdef ESP32 +#if defined(ESP32) || defined(HOST) std::lock_guard lock(_lock); #endif size_t hit = 0; @@ -1218,7 +1218,7 @@ AsyncWebSocket::SendStatus AsyncWebSocket::textAll(AsyncWebSocketMessageBuffer * } AsyncWebSocket::SendStatus AsyncWebSocket::textAll(AsyncWebSocketSharedBuffer buffer) { -#ifdef ESP32 +#if defined(ESP32) || defined(HOST) std::lock_guard lock(_lock); #endif size_t hit = 0; @@ -1310,7 +1310,7 @@ AsyncWebSocket::SendStatus AsyncWebSocket::binaryAll(AsyncWebSocketMessageBuffer return status; } AsyncWebSocket::SendStatus AsyncWebSocket::binaryAll(AsyncWebSocketSharedBuffer buffer) { -#ifdef ESP32 +#if defined(ESP32) || defined(HOST) std::lock_guard lock(_lock); #endif size_t hit = 0; diff --git a/src/AsyncWebSocket.h b/src/AsyncWebSocket.h index 99a7456e6..ab6874c45 100644 --- a/src/AsyncWebSocket.h +++ b/src/AsyncWebSocket.h @@ -382,13 +382,8 @@ class AsyncWebSocket : public AsyncWebHandler { AwsEventHandler _eventHandler; AwsHandshakeHandler _handshakeHandler; bool _enabled; -<<<<<<< HEAD -#ifdef ESP32 - mutable std::recursive_mutex _lock; -======= #if defined(ESP32) || defined(HOST) - mutable std::mutex _lock; ->>>>>>> 24f7507 (Support hosted builds using Arduino-Emulator) + mutable std::recursive_mutex _lock; #endif public: From 00a50cb49f33d4f9d85fecbd4704be74e6764f69 Mon Sep 17 00:00:00 2001 From: Mitch Bradley Date: Thu, 2 Apr 2026 13:03:34 -1000 Subject: [PATCH 4/5] Fixed some missing overrides and includes --- src/AsyncEventSource.h | 4 ++-- src/AsyncWebSocket.cpp | 1 + src/AsyncWebSocket.h | 4 ++-- src/ESPAsyncWebServer.h | 4 ++++ src/WebAuthentication.h | 2 ++ 5 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/AsyncEventSource.h b/src/AsyncEventSource.h index 2dd869d36..15d39892e 100644 --- a/src/AsyncEventSource.h +++ b/src/AsyncEventSource.h @@ -331,11 +331,11 @@ class AsyncEventSourceResponse : public AsyncWebServerResponse { public: AsyncEventSourceResponse(AsyncEventSource *server); - void _respond(AsyncWebServerRequest *request); + void _respond(AsyncWebServerRequest *request) override; size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time) override { return 0; }; - bool _sourceValid() const { + bool _sourceValid() const override { return true; } }; diff --git a/src/AsyncWebSocket.cpp b/src/AsyncWebSocket.cpp index 52331150e..c47b2cb03 100644 --- a/src/AsyncWebSocket.cpp +++ b/src/AsyncWebSocket.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #define STATE_FRAME_START 0 #define STATE_FRAME_MASK 1 diff --git a/src/AsyncWebSocket.h b/src/AsyncWebSocket.h index ab6874c45..bed17b9c1 100644 --- a/src/AsyncWebSocket.h +++ b/src/AsyncWebSocket.h @@ -498,11 +498,11 @@ class AsyncWebSocketResponse : public AsyncWebServerResponse { public: AsyncWebSocketResponse(const String &key, AsyncWebSocket *server); - void _respond(AsyncWebServerRequest *request); + void _respond(AsyncWebServerRequest *request) override ; size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time) override { return 0; }; - bool _sourceValid() const { + bool _sourceValid() const override { return true; } }; diff --git a/src/ESPAsyncWebServer.h b/src/ESPAsyncWebServer.h index 19544df96..d11b6e021 100644 --- a/src/ESPAsyncWebServer.h +++ b/src/ESPAsyncWebServer.h @@ -19,6 +19,10 @@ #include #include +#ifndef __unused +#define __unused __attribute__((unused)) +#endif + #if __has_include("ArduinoJson.h") #include diff --git a/src/WebAuthentication.h b/src/WebAuthentication.h index a4819d60a..4712ed584 100644 --- a/src/WebAuthentication.h +++ b/src/WebAuthentication.h @@ -5,6 +5,8 @@ #include "Arduino.h" +using namespace arduino; + bool checkBasicAuthentication(const char *header, const char *username, const char *password); bool checkDigestAuthentication( From 38dcb2a22e780fc05a6d76874b957e32f4a63e30 Mon Sep 17 00:00:00 2001 From: Mitch Bradley Date: Thu, 2 Apr 2026 14:21:12 -1000 Subject: [PATCH 5/5] Fixed connection closing state machine problems --- src/AsyncWebSocket.cpp | 7 ------- src/WebResponseImpl.h | 2 ++ src/WebResponses.cpp | 36 +++++++++++++++++++++++++++--------- 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/src/AsyncWebSocket.cpp b/src/AsyncWebSocket.cpp index c47b2cb03..4c3fc78cf 100644 --- a/src/AsyncWebSocket.cpp +++ b/src/AsyncWebSocket.cpp @@ -264,14 +264,7 @@ AsyncWebSocketClient::AsyncWebSocketClient(AsyncClient *client, AsyncWebSocket * _client->onDisconnect( [](void *r, AsyncClient *c) { ((AsyncWebSocketClient *)(r))->_onDisconnect(); -#if defined(HOST) - // Do NOT delete c here: in the host build, AsyncClient lifetime is managed by - // std::shared_ptr in AsyncTCPManager::_clients. The manager removes the - // shared_ptr on the next poll cycle once it sees _socket < 0 (set by _close()). - (void)c; -#else delete c; -#endif }, this ); diff --git a/src/WebResponseImpl.h b/src/WebResponseImpl.h index 32525d567..18eef5861 100644 --- a/src/WebResponseImpl.h +++ b/src/WebResponseImpl.h @@ -82,6 +82,8 @@ class AsyncAbstractResponse : public AsyncWebServerResponse { std::unique_ptr > _send_buffer; // buffer data size specifiers size_t _send_buffer_offset{0}, _send_buffer_len{0}; + // track if final chunk terminator has been queued for chunked responses + bool _finalChunkQueued{false}; size_t _readDataFromCacheOrContent(uint8_t *data, const size_t len); size_t _fillBufferAndProcessTemplates(uint8_t *buf, size_t maxLen); diff --git a/src/WebResponses.cpp b/src/WebResponses.cpp index 8f7f40722..f501780ff 100644 --- a/src/WebResponses.cpp +++ b/src/WebResponses.cpp @@ -340,6 +340,7 @@ void AsyncAbstractResponse::_respond(AsyncWebServerRequest *request) { addHeader(T_Connection, T_close, false); _assembleHead(_assembled_headers, request->version()); _state = RESPONSE_HEADERS; + _finalChunkQueued = false; // Reset for new response write_send_buffs(request, 0, 0); } @@ -447,6 +448,11 @@ size_t AsyncAbstractResponse::write_send_buffs(AsyncWebServerRequest *request, s } if (_chunked) { + if (_finalChunkQueued) { + // Final chunk terminator was queued and add()ed to TCP buffer; stop reading content + _state = RESPONSE_WAIT_ACK; + break; + } // HTTP 1.1 allows leading zeros in chunk length. Or spaces may be added. // See https://datatracker.ietf.org/doc/html/rfc9112#section-7.1 size_t const readLen = @@ -466,8 +472,9 @@ size_t AsyncAbstractResponse::write_send_buffs(AsyncWebServerRequest *request, s _send_buffer_len += readLen + 8; // set buffers's size to match added data _sentLength += readLen; // data is not sent yet, but we won't get a chance to count this later properly for chunked data if (!readLen) { - // last chunk? - _state = RESPONSE_END; + // This is the final chunk terminator ("0\r\n\r\n") + // Don't break - let loop continue so add() pushes it to TCP buffer next iteration + _finalChunkQueued = true; } } } else { @@ -483,13 +490,13 @@ size_t AsyncAbstractResponse::write_send_buffs(AsyncWebServerRequest *request, s if (readLen == 0) { // no more data to send - _state = RESPONSE_END; + _state = RESPONSE_WAIT_ACK; // Wait for ACK instead of ending immediately } else if (readLen != RESPONSE_TRY_AGAIN) { _send_buffer_len += readLen; // set buffers's size to match added data _sentLength += readLen; // data is not sent yet, but we need it to understand that it would be last block if (_sendContentLength && (_sentLength == _contentLength)) { // it was last piece of content - _state = RESPONSE_END; + _state = RESPONSE_WAIT_ACK; // Wait for ACKs to be processed before ending } } } @@ -499,8 +506,10 @@ size_t AsyncAbstractResponse::write_send_buffs(AsyncWebServerRequest *request, s request->client()->send(); _writtenLength += payloadlen; #if ASYNCWEBSERVER_USE_CHUNK_INFLIGHT - _in_flight += payloadlen; - --_in_flight_credit; // take a credit + if (payloadlen > 0) { + _in_flight += payloadlen; + --_in_flight_credit; // take a credit only when data is actually in-flight + } #endif if (_send_buffer_len == 0) { // buffer empty, we can release mem, otherwise need to keep it till next run (should not happen under normal conditions) @@ -511,9 +520,18 @@ size_t AsyncAbstractResponse::write_send_buffs(AsyncWebServerRequest *request, s // implicit check if (_state == RESPONSE_WAIT_ACK) { - // we do not need to wait for any acks actually if we won't send any more data, - // connection would be closed gracefully with last piece of data (in AsyncWebServerRequest::_onAck) - _state = RESPONSE_END; + // Only transition to END once all data has been acknowledged + if (_sendContentLength) { + // For fixed-length responses, make sure all content has been ACK'd + if (_ackedLength >= _contentLength) { + _state = RESPONSE_END; + } + } else { + // For streaming/chunked responses, once all buffered data is gone and sent, we're done + if (_send_buffer_len == 0) { + _state = RESPONSE_END; + } + } } return 0; }